├── .editorconfig ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .node-version ├── .npmignore ├── .nvmrc ├── .travis.yml ├── .yarnclean ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── examples ├── base.js └── console.css ├── index.html ├── package.json ├── scripts ├── banner.ejs ├── modules.rollup.config.js ├── test.rollup.config.js ├── umd.rollup.config.js └── version.js ├── src ├── plugin.js └── tracking │ ├── buffering.js │ ├── pause.js │ ├── percentile.js │ ├── performance.js │ ├── play.js │ └── seek.js ├── test ├── index.html ├── karma.conf.js └── plugin.test.js └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_style = space 8 | indent_size = 2 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## PR Description, what changed? 2 | 3 | - List out changes 4 | 5 | ## Steps for testing 6 | 7 | - List out any testing steps 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # OS 2 | Thumbs.db 3 | ehthumbs.db 4 | Desktop.ini 5 | .DS_Store 6 | ._* 7 | 8 | # Editors 9 | *~ 10 | *.swp 11 | *.tmproj 12 | *.tmproject 13 | *.sublime-* 14 | .idea/ 15 | .project/ 16 | .settings/ 17 | .vscode/ 18 | 19 | # Logs 20 | logs 21 | *.log 22 | npm-debug.log* 23 | 24 | # Dependency directories 25 | bower_components/ 26 | node_modules/ 27 | 28 | # Yeoman meta-data 29 | .yo-rc.json 30 | 31 | # Build-related directories 32 | dist/ 33 | docs/api/ 34 | test/dist/ 35 | .eslintcache 36 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | v17.2.0 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Intentionally left blank, so that npm does not ignore anything by default, 2 | # but relies on the package.json "files" array to explicitly define what ends 3 | # up in the package. 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v17.2.0 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | dist: trusty 3 | language: node_js 4 | node_js: 5 | - 'node' 6 | - 'lts/argon' 7 | before_script: 8 | 9 | # Check if the current version is equal to the major version for the env. 10 | - 'export IS_INSTALLED="$(npm list video.js | grep "video.js@$VJS")"' 11 | 12 | # We have to add semicolons to the end of each line in the if as Travis runs 13 | # this all on one line. 14 | - 'if [ -z "$IS_INSTALLED" ]; then 15 | echo "INSTALLING video.js@>=$VJS.0.0-RC.0 <$(($VJS+1)).0.0"; 16 | npm i "video.js@>=$VJS.0.0-RC.0 <\$(($VJS+1)).0.0"; 17 | else 18 | echo "video.js@$VJS ALREADY INSTALLED"; 19 | fi' 20 | - export CHROME_BIN=/usr/bin/google-chrome 21 | - export DISPLAY=:99.0 22 | - sh -e /etc/init.d/xvfb start 23 | env: 24 | - VJS=5 25 | - VJS=6 26 | - VJS=7 27 | addons: 28 | firefox: latest 29 | apt: 30 | sources: 31 | - google-chrome 32 | packages: 33 | - google-chrome-stable 34 | -------------------------------------------------------------------------------- /.yarnclean: -------------------------------------------------------------------------------- 1 | # test directories 2 | __tests__ 3 | test 4 | tests 5 | powered-test 6 | 7 | # asset directories 8 | docs 9 | doc 10 | website 11 | images 12 | assets 13 | 14 | # examples 15 | example 16 | examples 17 | 18 | # code coverage directories 19 | coverage 20 | .nyc_output 21 | 22 | # build scripts 23 | Makefile 24 | Gulpfile.js 25 | Gruntfile.js 26 | 27 | # configs 28 | .tern-project 29 | .gitattributes 30 | .editorconfig 31 | .*ignore 32 | .eslintrc 33 | .jshintrc 34 | .flowconfig 35 | .documentup.json 36 | .yarn-metadata.json 37 | .*.yml 38 | *.yml 39 | 40 | # misc 41 | *.gz 42 | *.md 43 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.3 (2022-03-21) 2 | 3 | - Removed eslintcache file 4 | - Adds a PR Template file GitHub 5 | - Changing dependency requirements for VideoJS. We I tested backwards to VideoJS 5.20.5 (release Feb 13, 2018) 6 | - The examples file will now display the version of videojs that we're using 7 | - Tracking the seek event using seeking 8 | - Updates pause event to depend on the videojs seeking() and/or scrubbing() functions 9 | - Reset the onSecondsToLoad in the Play event when the video is ended 10 | 11 | ## 1.0.2 12 | 13 | - Adds a failsafe to percentile never firing 14 | 15 | ## 1.0.1 16 | 17 | - [Adds an option for buffering data collection](https://github.com/spodlecki/videojs-event-tracking/pull/10) 18 | 19 | ## 1.0.0 20 | 21 | - Releasing 1.0.0 22 | - Testing with videojs 7 23 | - Updating package.json with a full upgrade 24 | 25 | ## 0.0.8 26 | 27 | - Hotfix: allowing use for videojs 5 and videojs 6 28 | 29 | ## 0.0.7 30 | 31 | - Updating to VideoJS6 32 | 33 | ## 0.0.6 34 | 35 | - Updating Performance Tracking to include a beforeunload event 36 | - Update Buffer Tracking to reset when the user pauses the stream 37 | - Update Play tracking to make sure secondsToLoad is reset to 0 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CONTRIBUTING 2 | 3 | We welcome contributions from everyone! 4 | 5 | ## Getting Started 6 | 7 | Make sure you have NodeJS 0.10 or higher and npm installed. 8 | 9 | 1. Fork this repository and clone your fork 10 | 1. Install dependencies: `npm install` 11 | 1. Run a development server: `npm start` 12 | 13 | ### Making Changes 14 | 15 | Refer to the [video.js plugin conventions][conventions] for more detail on best practices and tooling for video.js plugin authorship. 16 | 17 | When you've made your changes, push your commit(s) to your fork and issue a pull request against the original repository. 18 | 19 | ### Running Tests 20 | 21 | Testing is a crucial part of any software project. For all but the most trivial changes (typos, etc) test cases are expected. Tests are run in actual browsers using [Karma][karma]. 22 | 23 | - In all available and supported browsers: `npm test` 24 | - In a specific browser: `npm run test:chrome`, `npm run test:firefox`, etc. 25 | - While development server is running (`npm start`), navigate to [`http://localhost:9999/test/`][local] 26 | 27 | 28 | [karma]: http://karma-runner.github.io/ 29 | [local]: http://localhost:9999/test/ 30 | [conventions]: https://github.com/videojs/generator-videojs-plugin/blob/master/docs/conventions.md 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) spodlecki 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # videojs-event-tracking 2 | 3 | Track events with VideoJS and keep an eye on performance metrics. This has been tested with VideoJS 5 through 7, if you want to see if it works nicely with your version simply clone the repo and update package.json. Open `index.html` and press play -- watch the events stream through. 4 | 5 | ## Installation 6 | 7 | ```sh 8 | npm install --save videojs-event-tracking 9 | ``` 10 | 11 | ```sh 12 | yarn add videojs-event-tracking 13 | ``` 14 | 15 | ## Usage 16 | 17 | To include videojs-event-tracking on your website or web application, use any of the following methods. 18 | 19 | Initializing just like a normal videojs plugin does. 20 | 21 | ```javascript 22 | videojs('videodomid', {..., plugins: { eventTracking: true } }); 23 | // or 24 | videoInstance.eventTracking({... config ...}); 25 | ``` 26 | 27 | ## Current Events 28 | 29 | 30 | ### Play 31 | 32 | This event is triggered when the video has been played for the first time. If you are looking to track play events, simply listen on the player for a normal "play" or "playing" event. 33 | 34 | ```javascript 35 | player.on('tracking:firstplay', (e, data) => console.log(data)) 36 | ``` 37 | 38 | Data Attributes: 39 | 40 | * secondsToLoad: Total number of seconds between the player initializing a play request and when the first frame begins. 41 | 42 | ### Pausing 43 | 44 | Tracks when users pause the video. 45 | 46 | ```javascript 47 | player.on('tracking:pause', (e, data) => console.log(data)) 48 | ``` 49 | 50 | Data Attributes: 51 | 52 | * pauseCount: Total number of Pause events triggered 53 | 54 | ### Seeking 55 | 56 | During playback, we are tracking how many times a person seeks, and the position a user has seeked to. 57 | 58 | ```javascript 59 | player.on('tracking:seek', (e, data) => console.log(data)) 60 | ``` 61 | 62 | Data Attributes: 63 | 64 | * seekCount: total number of seeks that has occuring during this file 65 | * seekTo: Position, in seconds, that has been seeked to. 66 | 67 | ### Buffering 68 | 69 | Tracks when the video player is marked as buffering and waits until the player has made some progress. 70 | 71 | ```javascript 72 | player.on('tracking:buffered', (e, data) => console.log(data)) 73 | ``` 74 | 75 | Data Attributes: 76 | 77 | * currentTime: current second of video playback 78 | * readyState: video#readyState value 79 | * secondsToLoad: Total amount of time in seconds buffering took 80 | * bufferCount: Total buffer events for this source 81 | 82 | ### Positioning 83 | 84 | Track Overall Percentile (1st, 2nd, 3rd, and 4th) of Completion. This event triggers each quarter of a video. 85 | 86 | ```javascript 87 | player.on('tracking:first-quarter', (e, data) => console.log(data)) 88 | player.on('tracking:second-quarter', (e, data) => console.log(data)) 89 | player.on('tracking:third-quarter', (e, data) => console.log(data)) 90 | player.on('tracking:fourth-quarter', (e, data) => console.log(data)) 91 | ``` 92 | 93 | Data Attributes: 94 | 95 | * pauseCount: Total number of Pause events triggered 96 | * seekCount: Total number of Seek events triggered 97 | * currentTime: Current second video is on 98 | * duration: Total duration of video 99 | 100 | ### Performance 101 | 102 | _*note* a little experimental_ 103 | 104 | This event triggers when the player has changed sources, has ended, or has been destroyed. 105 | 106 | Data Attributes: 107 | 108 | * pauseCount: Total number of Pause events triggered 109 | * seekCount: Total number of Seek events triggered 110 | * bufferCount: Total number of Buffer events triggered 111 | * totalDuration: Total duration provided by the file 112 | * watchedDuration: Total number of seconds watched, this excluses seconds a user has seeked past. 113 | * bufferDuration: Total seconds that buffering has occured 114 | * initialLoadTime: Seconds it took for the initial frame to appear 115 | 116 | *Special Requirement* 117 | When initializing, you'll need to pass a function to the configuration for this plugin. 118 | 119 | ```javascript 120 | pluginConfig = { 121 | performance: function(data) { 122 | /** Use your preferred event tracking platform. 123 | * Google Analytics? Amplitude? Piwik? Mixpanel? 124 | */ 125 | } 126 | } 127 | ``` 128 | 129 | *Why?* 130 | 131 | In order to keep accuracy high, it listens for the [browser's `beforeunload`](https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event). 132 | While it is not completely accurate either, it does give us more opportunity to catch the actual performance data we're looking for. This functionality 133 | should be noted that there is potential for noise. 134 | -------------------------------------------------------------------------------- /examples/base.js: -------------------------------------------------------------------------------- 1 | (function(window, videojs) { 2 | var log = function(name, data) { 3 | var args = Array.from(arguments); 4 | var ele = document.getElementById('console'); 5 | var node = document.createElement('p'); 6 | node.innerHTML = name + ': ' + JSON.stringify(data); 7 | ele.innerHTML = node.outerHTML + ele.innerHTML; 8 | } 9 | 10 | var btn = document.getElementById('load') 11 | btn.addEventListener('click', function(e) { 12 | e.preventDefault(); 13 | player.autoplay(true); 14 | player.src([ 15 | { src: 'https://vjs.zencdn.net/v/oceans.mp4?' + Math.random(), type: 'video/mp4' }, 16 | { src: 'https://vjs.zencdn.net/v/oceans.webm?' + Math.random(), type: 'video/webm' } 17 | ]); 18 | }); 19 | 20 | var player = window.player = videojs('videojs-event-tracking-player', { 21 | poster: 'https://vjs.zencdn.net/v/oceans.png', 22 | sources: [ 23 | { src: 'https://vjs.zencdn.net/v/oceans.mp4', type: 'video/mp4' }, 24 | { src: 'https://vjs.zencdn.net/v/oceans.webm', type: 'video/webm' } 25 | ] 26 | }); 27 | 28 | player.eventTracking({ 29 | performance: function(data) { 30 | log('tracking:performance', data); 31 | }, 32 | /* 33 | // optional configuration to consider buffering while user is scrubbing on the video player. 34 | bufferingConfig: { 35 | includeScrub: true 36 | }*/ 37 | }); 38 | 39 | player.on('tracking:firstplay', function(e, data) { 40 | log(e.type, data); 41 | }); 42 | 43 | player.on('error', function(e) { 44 | log(e.type, { message: e.message }); 45 | }); 46 | 47 | player.on('tracking:play', function(e, data) { 48 | log(e.type, data); 49 | }); 50 | 51 | player.on('tracking:pause', function(e, data) { 52 | log(e.type, data); 53 | }); 54 | 55 | player.on('tracking:first-quarter', function(e, data) { 56 | log(e.type, data); 57 | }); 58 | 59 | player.on('tracking:second-quarter', function(e, data) { 60 | log(e.type, data); 61 | }); 62 | 63 | player.on('tracking:third-quarter', function(e, data) { 64 | log(e.type, data); 65 | }); 66 | 67 | player.on('tracking:fourth-quarter', function(e, data) { 68 | log(e.type, data); 69 | }); 70 | 71 | player.on('tracking:buffered', function(e, data) { 72 | log(e.type, data); 73 | }); 74 | 75 | player.on('tracking:seek', function(e, data) { 76 | log(e.type, data); 77 | }); 78 | 79 | log('videojs', { version: videojs.VERSION }); 80 | }(window, window.videojs)); 81 | -------------------------------------------------------------------------------- /examples/console.css: -------------------------------------------------------------------------------- 1 | #console { 2 | display: block; 3 | background-color: #2e2e2e; 4 | border-radius: 5px; 5 | padding: 10px; 6 | margin: 10px 0; 7 | } 8 | 9 | #console p { 10 | color: #9a9a9a; 11 | margin: 2px 0; 12 | } 13 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | videojs-event-tracking Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "videojs-event-tracking", 3 | "homepage": "https://github.com/spodlecki/videojs-event-tracking", 4 | "version": "1.0.3", 5 | "description": "Track events with VideoJS and keep an eye on performance metrics", 6 | "main": "dist/videojs-event-tracking.cjs.js", 7 | "module": "dist/videojs-event-tracking.es.js", 8 | "generator-videojs-plugin": { 9 | "version": "5.0.2" 10 | }, 11 | "scripts": { 12 | "prebuild": "npm run clean", 13 | "build": "npm-run-all -p build:*", 14 | "build:js": "npm-run-all build:js:rollup-modules build:js:rollup-umd build:js:bannerize build:js:uglify", 15 | "build:js:bannerize": "bannerize dist/videojs-event-tracking.js --banner=scripts/banner.ejs", 16 | "build:js:rollup-modules": "rollup -c scripts/modules.rollup.config.js", 17 | "build:js:rollup-umd": "rollup -c scripts/umd.rollup.config.js", 18 | "build:js:uglify": "uglifyjs dist/videojs-event-tracking.js --comments --mangle --compress -o dist/videojs-event-tracking.min.js", 19 | "build:test": "rollup -c scripts/test.rollup.config.js", 20 | "clean": "rimraf dist test/dist", 21 | "postclean": "mkdirp dist test/dist", 22 | "lint": "vjsstandard", 23 | "start": "npm-run-all -p start:server watch", 24 | "start:server": "static -a 0.0.0.0 -p 9999 -H '{\"Cache-Control\": \"no-cache, must-revalidate\"}' .", 25 | "pretest": "npm-run-all lint build", 26 | "test": "karma start test/karma.conf.js", 27 | "preversion": "npm run test", 28 | "version": "node scripts/version.js", 29 | "watch": "npm-run-all -p watch:*", 30 | "watch:js-modules": "rollup -c scripts/modules.rollup.config.js -w", 31 | "watch:js-umd": "rollup -c scripts/umd.rollup.config.js -w", 32 | "watch:test": "rollup -c scripts/test.rollup.config.js -w", 33 | "prepublish": "npm run build", 34 | "prepush": "npm run test" 35 | }, 36 | "keywords": [ 37 | "videojs", 38 | "videojs-plugin" 39 | ], 40 | "author": "spodlecki ", 41 | "license": "MIT", 42 | "vjsstandard": { 43 | "ignore": [ 44 | "dist", 45 | "docs", 46 | "test/dist", 47 | "examples", 48 | "test/karma.conf.js" 49 | ] 50 | }, 51 | "files": [ 52 | "CONTRIBUTING.md", 53 | "dist/", 54 | "docs/", 55 | "index.html", 56 | "scripts/", 57 | "src/", 58 | "test/" 59 | ], 60 | "dependencies": { 61 | "video.js": ">=5.20.5" 62 | }, 63 | "devDependencies": { 64 | "babel-plugin-external-helpers": "^6.22.0", 65 | "babel-plugin-transform-object-assign": "^6.8.0", 66 | "babel-preset-es2015": "^6.14.0", 67 | "bannerize": "^1.0.2", 68 | "conventional-changelog-cli": "^1.3.1", 69 | "conventional-changelog-videojs": "^3.0.0", 70 | "husky": "^0.13.3", 71 | "karma": "^6.3.16", 72 | "karma-chrome-launcher": "^2.1.1", 73 | "karma-detect-browsers": "^2.2.5", 74 | "karma-firefox-launcher": "^1.0.1", 75 | "karma-ie-launcher": "^1.0.0", 76 | "karma-qunit": "^1.2.1", 77 | "mkdirp": "^0.5.1", 78 | "node-static": "^0.7.9", 79 | "npm-run-all": "^4.0.2", 80 | "qunitjs": "^2.3.2", 81 | "rimraf": "^2.6.1", 82 | "rollup": "^0.41.6", 83 | "rollup-plugin-babel": "^2.7.1", 84 | "rollup-plugin-commonjs": "^8.0.2", 85 | "rollup-plugin-json": "^2.1.1", 86 | "rollup-plugin-multi-entry": "^2.0.1", 87 | "rollup-plugin-node-resolve": "^3.0.0", 88 | "rollup-watch": "^3.2.2", 89 | "semver": "^5.3.0", 90 | "sinon": "^2.2.0", 91 | "uglify-js": "^3.0.7", 92 | "videojs-standard": "^7.0.0" 93 | }, 94 | "eslintConfig": { 95 | "env": { 96 | "browser": true, 97 | "node": true 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /scripts/banner.ejs: -------------------------------------------------------------------------------- 1 | /** 2 | * <%- pkg.name %> 3 | * @version <%- pkg.version %> 4 | * @copyright <%- date.getFullYear() %> <%- pkg.author %> 5 | * @license <%- pkg.license %> 6 | */ 7 | -------------------------------------------------------------------------------- /scripts/modules.rollup.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rollup configuration for packaging the plugin in a module that is consumable 3 | * by either CommonJS (e.g. Node or Browserify) or ECMAScript (e.g. Rollup). 4 | * 5 | * These modules DO NOT include their dependencies as we expect those to be 6 | * handled by the module system. 7 | */ 8 | import babel from 'rollup-plugin-babel'; 9 | import json from 'rollup-plugin-json'; 10 | 11 | export default { 12 | moduleName: 'videojsEventTracking', 13 | entry: 'src/plugin.js', 14 | external: [ 15 | 'global', 16 | 'global/document', 17 | 'global/window', 18 | 'video.js' 19 | ], 20 | globals: { 21 | 'video.js': 'videojs' 22 | }, 23 | legacy: true, 24 | plugins: [ 25 | json(), 26 | babel({ 27 | babelrc: false, 28 | exclude: 'node_modules/**', 29 | presets: [ 30 | ['es2015', { 31 | loose: true, 32 | modules: false 33 | }] 34 | ], 35 | plugins: [ 36 | 'external-helpers', 37 | 'transform-object-assign' 38 | ] 39 | }) 40 | ], 41 | targets: [ 42 | {dest: 'dist/videojs-event-tracking.cjs.js', format: 'cjs'}, 43 | {dest: 'dist/videojs-event-tracking.es.js', format: 'es'} 44 | ] 45 | }; 46 | -------------------------------------------------------------------------------- /scripts/test.rollup.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rollup configuration for packaging the plugin in a test bundle. 3 | * 4 | * This includes all dependencies for both the plugin and its tests. 5 | */ 6 | import babel from 'rollup-plugin-babel'; 7 | import commonjs from 'rollup-plugin-commonjs'; 8 | import json from 'rollup-plugin-json'; 9 | import multiEntry from 'rollup-plugin-multi-entry'; 10 | import resolve from 'rollup-plugin-node-resolve'; 11 | 12 | export default { 13 | moduleName: 'videojsEventTrackingTests', 14 | entry: 'test/plugin.test.js', 15 | dest: 'test/dist/bundle.js', 16 | format: 'iife', 17 | external: [ 18 | 'qunit', 19 | 'qunitjs', 20 | 'sinon', 21 | 'video.js' 22 | ], 23 | globals: { 24 | 'qunit': 'QUnit', 25 | 'qunitjs': 'QUnit', 26 | 'sinon': 'sinon', 27 | 'video.js': 'videojs' 28 | }, 29 | legacy: true, 30 | plugins: [ 31 | multiEntry({ 32 | exports: false 33 | }), 34 | resolve({ 35 | browser: true, 36 | main: true, 37 | jsnext: true 38 | }), 39 | json(), 40 | commonjs({ 41 | sourceMap: false 42 | }), 43 | babel({ 44 | babelrc: false, 45 | exclude: 'node_modules/**', 46 | presets: [ 47 | ['es2015', { 48 | loose: true, 49 | modules: false 50 | }] 51 | ], 52 | plugins: [ 53 | 'external-helpers', 54 | 'transform-object-assign' 55 | ] 56 | }) 57 | ] 58 | }; 59 | -------------------------------------------------------------------------------- /scripts/umd.rollup.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Rollup configuration for packaging the plugin in a module that is consumable 3 | * as the `src` of a `script` tag or via AMD or similar client-side loading. 4 | * 5 | * This module DOES include its dependencies. 6 | */ 7 | import babel from 'rollup-plugin-babel'; 8 | import commonjs from 'rollup-plugin-commonjs'; 9 | import json from 'rollup-plugin-json'; 10 | import resolve from 'rollup-plugin-node-resolve'; 11 | 12 | export default { 13 | moduleName: 'videojsEventTracking', 14 | entry: 'src/plugin.js', 15 | dest: 'dist/videojs-event-tracking.js', 16 | format: 'umd', 17 | external: ['video.js'], 18 | globals: { 19 | 'video.js': 'videojs' 20 | }, 21 | legacy: true, 22 | plugins: [ 23 | resolve({ 24 | browser: true, 25 | main: true, 26 | jsnext: true 27 | }), 28 | json(), 29 | commonjs({ 30 | sourceMap: false 31 | }), 32 | babel({ 33 | babelrc: false, 34 | exclude: 'node_modules/**', 35 | presets: [ 36 | ['es2015', { 37 | loose: true, 38 | modules: false 39 | }] 40 | ], 41 | plugins: [ 42 | 'external-helpers', 43 | 'transform-object-assign' 44 | ] 45 | }) 46 | ] 47 | }; 48 | -------------------------------------------------------------------------------- /scripts/version.js: -------------------------------------------------------------------------------- 1 | const execSync = require('child_process').execSync; 2 | const path = require('path'); 3 | const semver = require('semver'); 4 | const pkg = require('../package.json'); 5 | 6 | if (!semver.prerelease(pkg.version)) { 7 | process.chdir(path.resolve(__dirname, '..')); 8 | execSync('conventional-changelog -p videojs -i CHANGELOG.md -s'); 9 | execSync('git add CHANGELOG.md'); 10 | } 11 | -------------------------------------------------------------------------------- /src/plugin.js: -------------------------------------------------------------------------------- 1 | import videojs from 'video.js'; 2 | import {version as VERSION} from '../package.json'; 3 | 4 | import BufferTracking from './tracking/buffering'; 5 | import PauseTracking from './tracking/pause'; 6 | import PositionTracking from './tracking/percentile'; 7 | import PerformanceTracking from './tracking/performance'; 8 | import PlayTracking from './tracking/play'; 9 | import SeekTracking from './tracking/seek'; 10 | 11 | // Cross-compatibility for Video.js 5 and 6. 12 | const registerPlugin = videojs.registerPlugin || videojs.plugin; 13 | const getPlugin = videojs.getPlugin || videojs.plugin; 14 | 15 | /** 16 | * Event Tracking for VideoJS 17 | * 18 | * @function eventTracking 19 | * @param {Object} [options={}] 20 | * An object of options left to the plugin author to define. 21 | */ 22 | const eventTracking = function(options) { 23 | PauseTracking.apply(this, arguments); 24 | BufferTracking.apply(this, arguments); 25 | PositionTracking.apply(this, arguments); 26 | PlayTracking.apply(this, arguments); 27 | SeekTracking.apply(this, arguments); 28 | PerformanceTracking.apply(this, arguments); 29 | }; 30 | 31 | // Register the plugin with video.js, avoid double registration 32 | if (typeof getPlugin('eventTracking') === 'undefined') { 33 | registerPlugin('eventTracking', eventTracking); 34 | } 35 | 36 | // Include the version number. 37 | eventTracking.VERSION = VERSION; 38 | 39 | export default eventTracking; 40 | -------------------------------------------------------------------------------- /src/tracking/buffering.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @function BufferTracking 3 | * @param {Object} [options={}] 4 | * An object of options left to the plugin author to define. 5 | * 6 | * Can contain the following optional configuration, passed during plugin initialization: 7 | * bufferingConfig.includeScrub => Boolean indicating whether buffering metrics 8 | * should be considered for computation while user is scrubbing on the video player. 9 | * 10 | * 11 | * Tracks when the video player is marked as buffering and waits until the player 12 | * has made some progress. 13 | * 14 | * Example Usage: 15 | * player.on('tracking:buffered', (e, data) => console.log(data)) 16 | * 17 | * Data Attributes: 18 | * => currentTime: current second of video playback 19 | * => readyState: video#readyState value 20 | * => secondsToLoad: Total amount of time in seconds buffering took 21 | * => bufferCount: Total buffer events for this source 22 | */ 23 | 24 | const BufferTracking = function(config) { 25 | let timer = null; 26 | let scrubbing = false; 27 | let bufferPosition = false; 28 | let bufferStart = false; 29 | let bufferEnd = false; 30 | let bufferCount = 0; 31 | let readyState = false; 32 | 33 | const reset = function() { 34 | if (timer) { 35 | clearTimeout(timer); 36 | } 37 | scrubbing = false; 38 | bufferPosition = false; 39 | bufferStart = false; 40 | bufferEnd = false; 41 | bufferCount = 0; 42 | readyState = false; 43 | }; 44 | 45 | const onPause = () => { 46 | bufferStart = false; 47 | 48 | if (this.scrubbing() && !(config.bufferingConfig && config.bufferingConfig.includeScrub)) { 49 | scrubbing = true; 50 | timer = setTimeout(function() { 51 | scrubbing = false; 52 | }, 200); 53 | } 54 | }; 55 | 56 | const onPlayerWaiting = () => { 57 | if (bufferStart === false && scrubbing === false && this.currentTime() > 0) { 58 | bufferStart = new Date(); 59 | bufferPosition = +this.currentTime().toFixed(0); 60 | readyState = +this.readyState(); 61 | } 62 | }; 63 | 64 | const onTimeupdate = () => { 65 | const curTime = +this.currentTime().toFixed(0); 66 | 67 | if (bufferStart && curTime !== bufferPosition) { 68 | bufferEnd = new Date(); 69 | 70 | const secondsToLoad = ((bufferEnd - bufferStart) / 1000); 71 | 72 | bufferStart = false; 73 | bufferPosition = false; 74 | bufferCount++; 75 | 76 | this.trigger('tracking:buffered', { 77 | currentTime: +curTime, 78 | readyState: +readyState, 79 | secondsToLoad: +secondsToLoad.toFixed(3), 80 | bufferCount: +bufferCount 81 | }); 82 | } 83 | }; 84 | 85 | this.on('dispose', reset); 86 | this.on('loadstart', reset); 87 | this.on('ended', reset); 88 | this.on('pause', onPause); 89 | this.on('waiting', onPlayerWaiting); 90 | this.on('timeupdate', onTimeupdate); 91 | }; 92 | 93 | export default BufferTracking; 94 | -------------------------------------------------------------------------------- /src/tracking/pause.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Tracks when users pause the video. 3 | * 4 | * Example Usage: 5 | * player.on('tracking:pause', (e, data) => console.log(data)) 6 | * 7 | * Data Attributes: 8 | * => pauseCount: Total number of Pause events triggered 9 | * 10 | * @function PauseTracking 11 | * @param {Object} [config={}] 12 | * An object of config left to the plugin author to define. 13 | */ 14 | 15 | const PauseTracking = function(config) { 16 | const player = this; 17 | let pauseCount = 0; 18 | let timer = null; 19 | let locked = false; 20 | 21 | const reset = function(e) { 22 | if (timer) { 23 | clearTimeout(timer); 24 | } 25 | pauseCount = 0; 26 | locked = false; 27 | }; 28 | 29 | const isSeeking = function() { 30 | return ( 31 | typeof (player.seeking) === 'function' && player.seeking() || 32 | typeof (player.scrubbing) === 'function' && player.scrubbing() 33 | ); 34 | }; 35 | 36 | player.on('dispose', reset); 37 | player.on('loadstart', reset); 38 | player.on('ended', reset); 39 | player.on('pause', function() { 40 | if (isSeeking() || locked) { 41 | return; 42 | } 43 | 44 | timer = setTimeout(function() { 45 | pauseCount++; 46 | player.trigger('tracking:pause', {pauseCount}); 47 | }, 300); 48 | }); 49 | }; 50 | 51 | export default PauseTracking; 52 | -------------------------------------------------------------------------------- /src/tracking/percentile.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Track Overall Percentile (1st, 2nd, 3rd, and 4th) of Completion 3 | * This event triggers each quarter of a video. 4 | * 5 | * Example Usage: 6 | * player.on('tracking:first-quarter', (e, data) => console.log(data)) 7 | * player.on('tracking:second-quarter', (e, data) => console.log(data)) 8 | * player.on('tracking:third-quarter', (e, data) => console.log(data)) 9 | * player.on('tracking:fourth-quarter', (e, data) => console.log(data)) 10 | * 11 | * Data Attributes: 12 | * => pauseCount: Total number of Pause events triggered 13 | * => seekCount: Total number of Seek events triggered 14 | * => currentTime: Current second video is on 15 | * => duration: Total duration of video 16 | * 17 | * @function PercentileTracking 18 | * @param {Object} [config={}] 19 | * An object of config left to the plugin author to define. 20 | */ 21 | 22 | const PercentileTracking = function(config) { 23 | const player = this; 24 | let first = false; 25 | let second = false; 26 | let third = false; 27 | let duration = null; 28 | let pauseCount = 0; 29 | let seekCount = 0; 30 | 31 | const reset = function(e) { 32 | first = false; 33 | second = false; 34 | third = false; 35 | duration = null; 36 | pauseCount = 0; 37 | seekCount = 0; 38 | }; 39 | 40 | const incPause = () => pauseCount++; 41 | const incSeek = () => seekCount++; 42 | 43 | const getDuration = function() { 44 | duration = +player.duration().toFixed(0); 45 | if (duration > 0) { 46 | const quarter = (duration / 4).toFixed(0); 47 | 48 | first = +quarter; 49 | second = +quarter * 2; 50 | third = +quarter * 3; 51 | } 52 | }; 53 | 54 | player.on('dispose', reset); 55 | player.on('loadstart', reset); 56 | player.on('tracking:pause', incPause); 57 | player.on('tracking:seek', incSeek); 58 | player.on('timeupdate', function() { 59 | if (duration === null) { 60 | getDuration(); 61 | } 62 | 63 | const curTime = +player.currentTime().toFixed(0); 64 | const data = { 65 | seekCount, 66 | pauseCount, 67 | currentTime: curTime, 68 | duration 69 | }; 70 | 71 | switch (curTime) { 72 | case first: 73 | first = false; 74 | player.trigger('tracking:first-quarter', data); 75 | break; 76 | case second: 77 | second = false; 78 | player.trigger('tracking:second-quarter', data); 79 | break; 80 | case third: 81 | third = false; 82 | player.trigger('tracking:third-quarter', data); 83 | break; 84 | } 85 | 86 | }); 87 | player.on('ended', function() { 88 | const data = { 89 | seekCount, 90 | pauseCount, 91 | currentTime: duration, 92 | duration 93 | }; 94 | 95 | player.trigger('tracking:fourth-quarter', data); 96 | }); 97 | 98 | player.on('durationchange', getDuration); 99 | }; 100 | 101 | export default PercentileTracking; 102 | -------------------------------------------------------------------------------- /src/tracking/performance.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Track Overall Performance 3 | * This event triggers when the player has changed sources, has ended, or 4 | * has been destroyed. 5 | * 6 | * Example Usage: 7 | * player.eventTracking({ 8 | * performance: function(data) { 9 | * // tracking here... 10 | * }, 11 | * }); 12 | * 13 | * Data Attributes: 14 | * => pauseCount: Total number of Pause events triggered 15 | * => seekCount: Total number of Seek events triggered 16 | * => bufferCount: Total number of Buffer events triggered 17 | * => totalDuration: Total duration provided by the file 18 | * => watchedDuration: Total number of seconds watched (not seeked past) 19 | * => bufferDuration: Total seconds that buffering has occured 20 | * => initialLoadTime: Seconds it took for the initial frame to appear 21 | * 22 | * @function PerformanceTracking 23 | * @param {Object} [config={}] 24 | * An object of config left to the plugin author to define. 25 | */ 26 | const PerformanceTracking = function(config) { 27 | if (typeof config === 'undefined') { 28 | return; 29 | } 30 | 31 | const player = this; 32 | let seekCount = 0; 33 | let pauseCount = 0; 34 | let bufferCount = 0; 35 | let totalDuration = 0; 36 | let watchedDuration = 0; 37 | let bufferDuration = 0; 38 | let initialLoadTime = 0; 39 | let timestamps = []; 40 | 41 | const reset = function() { 42 | seekCount = 0; 43 | pauseCount = 0; 44 | bufferCount = 0; 45 | totalDuration = 0; 46 | watchedDuration = 0; 47 | bufferDuration = 0; 48 | initialLoadTime = 0; 49 | timestamps = []; 50 | }; 51 | 52 | const trigger = function() { 53 | const data = { 54 | pauseCount, 55 | seekCount, 56 | bufferCount, 57 | totalDuration, 58 | watchedDuration, 59 | bufferDuration, 60 | initialLoadTime 61 | }; 62 | 63 | // warning: using this event instead of the function will reduce the accuracy 64 | // when a user refreshes the browser or closes, the beforeunload 65 | // event will become a race condition. 66 | player.trigger('tracking:performance', data); 67 | 68 | if (typeof config.performance === 'function') { 69 | config.performance.call(player, data); 70 | } 71 | }; 72 | 73 | const triggerAndReset = function() { 74 | trigger(); 75 | reset(); 76 | }; 77 | 78 | if (typeof window.addEventListener === 'function') { 79 | window.addEventListener('beforeunload', triggerAndReset); 80 | player.on('dispose', function() { 81 | window.removeEventListener('beforeunload', triggerAndReset); 82 | }); 83 | } 84 | 85 | player.on('loadstart', function() { 86 | if (totalDuration > 0) { 87 | trigger(); 88 | } 89 | 90 | reset(); 91 | }); 92 | 93 | player.on('ended', triggerAndReset); 94 | player.on('dispose', triggerAndReset); 95 | player.on('timeupdate', function() { 96 | const curTime = +player.currentTime().toFixed(0); 97 | 98 | if (timestamps.indexOf(curTime) < 0) { 99 | timestamps.push(curTime); 100 | } 101 | watchedDuration = timestamps.length; 102 | }); 103 | player.on('loadeddata', function(e, data) { 104 | totalDuration = +player.duration().toFixed(0); 105 | }); 106 | player.on('tracking:seek', function(e, data) { 107 | seekCount = data.seekCount; 108 | }); 109 | player.on('tracking:pause', function(e, data) { 110 | pauseCount = data.pauseCount; 111 | }); 112 | player.on('tracking:buffered', function(e, data) { 113 | ({ bufferCount } = data); 114 | bufferDuration = +(bufferDuration + data.secondsToLoad).toFixed(3); 115 | }); 116 | player.on('tracking:firstplay', function(e, data) { 117 | initialLoadTime = data.secondsToLoad; 118 | }); 119 | }; 120 | 121 | export default PerformanceTracking; 122 | -------------------------------------------------------------------------------- /src/tracking/play.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Track Initial Play Event 3 | * This event is triggered when the video has been played for the first time. 4 | * If you are looking to track play events, simply listen on the player for a normal 5 | * "play" or "playing" event. 6 | * 7 | * Example Usage: 8 | * player.on('tracking:firstplay', (e, data) => console.log(data)) 9 | * 10 | * Data Attributes: 11 | * => secondsToLoad: Total number of seconds between the player initializing 12 | * a play request and when the first frame begins. 13 | * 14 | * @function PlayTracking 15 | * @param {Object} [config={}] 16 | * An object of config left to the plugin author to define. 17 | */ 18 | 19 | const PlayTracking = function(config) { 20 | let hasBeenTriggered = false; 21 | let loadstart = 0; 22 | let loadend = 0; 23 | let secondsToLoad = 0; 24 | 25 | const reset = function() { 26 | hasBeenTriggered = false; 27 | loadstart = 0; 28 | loadend = 0; 29 | secondsToLoad = 0; 30 | }; 31 | 32 | const onLoadStart = function() { 33 | reset(); 34 | loadstart = new Date(); 35 | }; 36 | 37 | const onLoadedData = function() { 38 | loadend = new Date(); 39 | secondsToLoad = ((loadend - loadstart) / 1000); 40 | }; 41 | 42 | const onPlaying = () => { 43 | if (!hasBeenTriggered) { 44 | hasBeenTriggered = true; 45 | this.trigger('tracking:firstplay', { 46 | secondsToLoad: +(secondsToLoad.toFixed(3)) 47 | }); 48 | } 49 | }; 50 | 51 | this.on('ended', reset); 52 | this.on('dispose', reset); 53 | this.on('loadstart', onLoadStart); 54 | this.on('loadeddata', onLoadedData); 55 | this.on('playing', onPlaying); 56 | }; 57 | 58 | export default PlayTracking; 59 | -------------------------------------------------------------------------------- /src/tracking/seek.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Track Seeking Events 3 | * During playback, we are tracking how many times a person seeks, and 4 | * the position a user has seeked to. 5 | * 6 | * Example Usage: 7 | * player.on('tracking:seek', (e, data) => console.log(data)) 8 | * 9 | * Data Attributes: 10 | * => seekCount: total number of seeks that has occuring during this file 11 | * => seekTo: Position, in seconds, that has been seeked to. 12 | * 13 | * @function SeekTracking 14 | * @param {Object} [config={}] 15 | * An object of config left to the plugin author to define. 16 | */ 17 | const SeekTracking = function(config) { 18 | const player = this; 19 | let seekCount = 0; 20 | 21 | const reset = function() { 22 | seekCount = 0; 23 | }; 24 | 25 | player.on('dispose', reset); 26 | player.on('loadstart', reset); 27 | player.on('ended', reset); 28 | player.on('seeked', function() { 29 | const curTime = +player.currentTime().toFixed(0); 30 | 31 | seekCount++; 32 | player.trigger('tracking:seek', { 33 | seekCount: +seekCount, 34 | seekTo: curTime 35 | }); 36 | }); 37 | }; 38 | 39 | export default SeekTracking; 40 | -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | videojs-event-tracking Unit Tests 6 | 7 | 8 | 9 | 10 |
11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | var detectBrowsers = { 3 | enabled: false, 4 | usePhantomJS: false 5 | }; 6 | 7 | config.set({ 8 | basePath: '..', 9 | frameworks: ['qunit'], 10 | files: [ 11 | 'node_modules/video.js/dist/video-js.css', 12 | 'node_modules/sinon/pkg/sinon.js', 13 | 'node_modules/video.js/dist/video.js', 14 | 'test/dist/bundle.js' 15 | ], 16 | customLaunchers: { 17 | travisChrome: { 18 | base: 'Chrome', 19 | flags: ['--no-sandbox'] 20 | } 21 | }, 22 | browsers: ['Firefox', 'travisChrome'], 23 | reporters: ['dots'], 24 | port: 9876, 25 | colors: true, 26 | autoWatch: false, 27 | singleRun: true, 28 | concurrency: Infinity 29 | }); 30 | }; 31 | -------------------------------------------------------------------------------- /test/plugin.test.js: -------------------------------------------------------------------------------- 1 | import document from 'global/document'; 2 | 3 | import QUnit from 'qunit'; 4 | import sinon from 'sinon'; 5 | import videojs from 'video.js'; 6 | 7 | import plugin from '../src/plugin'; 8 | 9 | const Player = videojs.getComponent('Player'); 10 | 11 | QUnit.test('the environment is sane', function(assert) { 12 | assert.strictEqual(typeof Array.isArray, 'function', 'es5 exists'); 13 | assert.strictEqual(typeof sinon, 'object', 'sinon exists'); 14 | assert.strictEqual(typeof videojs, 'function', 'videojs exists'); 15 | assert.strictEqual(typeof plugin, 'function', 'plugin is a function'); 16 | }); 17 | 18 | QUnit.module('videojs-event-tracking', { 19 | 20 | beforeEach() { 21 | 22 | // Mock the environment's timers because certain things - particularly 23 | // player readiness - are asynchronous in video.js 5. This MUST come 24 | // before any player is created; otherwise, timers could get created 25 | // with the actual timer methods! 26 | this.clock = sinon.useFakeTimers(); 27 | 28 | this.fixture = document.getElementById('qunit-fixture'); 29 | this.video = document.createElement('video'); 30 | this.fixture.appendChild(this.video); 31 | this.player = videojs(this.video); 32 | }, 33 | 34 | afterEach() { 35 | this.player.dispose(); 36 | this.clock.restore(); 37 | } 38 | }); 39 | 40 | QUnit.test('registers itself with video.js', function(assert) { 41 | assert.expect(1); 42 | 43 | assert.strictEqual( 44 | typeof Player.prototype.eventTracking, 45 | 'function', 46 | 'videojs-event-tracking plugin was registered' 47 | ); 48 | 49 | this.player.eventTracking(); 50 | }); 51 | --------------------------------------------------------------------------------