Many businesses want to track how viewers engage with their videos – from how many times a video is played to where viewers are watching from. While professional platforms like VdoCipher offer advanced analytics out-of-the-box, it’s also possible to build a basic video analytics dashboard on your own.
In this guide you’ll:
- Collect the core metrics most teams care about (view event, device/browser/OS, geography, watch completion, basic QoE signals).
- Render the collected rows as JSON on the page (simple and copy-paste friendly).
- (Optional) Export that JSON or pipe it into Google Sheets later for charts/pivots & make your own Video Analytics Dashboard.
Key Video Metrics to Track (and why they matter)
- Device / OS / Browser
Shows where your audience really lives (mobile vs desktop, OS/browser splits). Use this to prioritize testing and UI decisions. - Geographic Distribution (country)
Helps with localization (subtitles/languages), and can surface unexpected markets. - Watch Completion / Average Watch %
Measures how much of a video is typically consumed. Low percentages often point to content or onboarding issues; high percentages suggest strong engagement. - Page URL – The page on which video is being watched.
- Video Duration – Total length of video.
- Time to Start (ms) (QoE): time between the first play() and the first playing frame—good “startup delay” proxy.
- Errors (QoE): surface MediaError codes to quickly debug real viewer failures.
Step 1 – Add a native HTML5 video
Use the stock <video> element with a self-hosted MP4 (or HLS/DASH with a JS loader if needed). Controls are on to keep things simple. This section defines the HTML structure – the <video> tag for playback, a button to trigger analytics export, and a <pre> block to show the resulting JSON.
Think of this as the “canvas” for all analytics code to act upon.
<video id="myVideo" width="640" height="360" controls> <source src="https://www.vdocipher.com/blog/wp-content/uploads/2024/02/livestream-start.mp4" type="video/mp4"> Your browser does not support HTML5 video. </video> <button id="publishJson" style="margin-top:12px;">Publish Analytics JSON</button> <pre id="analyticsJson" style="display:none;padding:10px;background:#f6f8fa;border-radius:8px;max-width:900px;overflow:auto;"></pre>
Step 2 – Start JS & Wrap everything in DOMContentLoaded including Config + tiny utils
This ensures that your script only runs after the DOM is fully loaded. By wrapping everything in a DOMContentLoaded listener, you guarantee that your script won’t fail because the <video> element hasn’t yet been rendered. Ensure, the script to be loaded
The basic utility functions are set up:
- VIDEO_ID uniquely identifies the video (useful for tracking multiple videos).
- analyticsRows is an array to store one JSON entry per session.
- uid() generates a unique session identifier using browser cryptography — ensuring every view is unique.
<script>
document.addEventListener('DOMContentLoaded', () => {
const video = document.getElementById('myVideo');
const publishBtn = document.getElementById('publishJson');
const out = document.getElementById('analyticsJson');
const VIDEO_ID = 'livestream-start';
const analyticsRows = [];
const now = () => performance.now();
function uid() {
return ([1e7]+-1e3+-4e3+-8e3+-1e11)
.replace(/[018]/g, c =>
(c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16)
);
}
// Place the remaining sections **inside** this callback, in the same order.
});
</script>
Step 3 – Device, OS, and Browser detection
This section parses navigator.userAgent to detect:
- Device type — mobile or desktop
- Operating system — Windows, macOS, Android, etc.
- Browser — Chrome, Firefox, Safari, Edge, etc.
This information helps in understanding playback performance across user environments.
function getDeviceInfo() {
const ua = navigator.userAgent;
const device = /Mobi|Android|iPhone|iPad|Tablet/i.test(ua) ? 'Mobile' : 'Desktop';
let os = 'Unknown OS';
if (ua.includes('Windows')) os = 'Windows';
else if (ua.includes('Mac')) os = 'macOS';
else if (ua.includes('Android')) os = 'Android';
else if (/iPhone|iPad|iPod/.test(ua)) os = 'iOS';
else if (ua.includes('Linux') || ua.includes('X11')) os = 'Linux/Unix';
let browser = 'Unknown Browser';
if (ua.includes('Chrome') && !ua.includes('Edg/')) browser = 'Chrome';
else if (ua.includes('Edg/')) browser = 'Edge';
else if (ua.includes('Firefox')) browser = 'Firefox';
else if (ua.includes('Safari') && !ua.includes('Chrome')) browser = 'Safari';
else if (ua.includes('OPR') || ua.includes('Opera')) browser = 'Opera';
else if (ua.includes('Trident') || ua.includes('MSIE')) browser = 'Internet Explorer';
return { device, os, browser };
}
Step 4 – Country lookup via IP (minimal geo)
The code uses the free ipapi.co service to fetch the viewer’s country and country code from their IP address.
It runs asynchronously, filling in the geo fields for each analytics entry. This helps you map your audience distribution geographically.
function getGeoLocation() {
return fetch('https://ipapi.co/json/')
.then(r => r.json())
.then(d => ({
country: d && d.country_name ? d.country_name : 'Unknown',
countryCode: d && d.country_code ? d.country_code : ''
}))
.catch(() => ({ country: 'Unknown', countryCode: '' }));
}
Step 5 – Session initialization
Here, all analytics fields are initialized — creating a “session object” that represents one playback event.
It includes general metadata (timestamp, video ID, URL) and playback variables like:
- watchPercent
- errors
Initially, many of these are blank and get populated as playback continues.
const session = {
timestamp: new Date().toISOString(),
sessionId: uid(),
videoId: VIDEO_ID,
pageUrl: location.href,
// engagement core
event: 'init',
completed: false,
maxTimeWatched: 0,
watchPercent: 0,
duration: null,
// device
...getDeviceInfo(),
// geo (filled async below)
country: 'Unknown',
countryCode: '',
// QoE basics kept
timeToStartMs: null,
// errors encountered
errors: []
};
// fill geo asynchronously
getGeoLocation().then(loc => {
session.country = loc.country;
session.countryCode = loc.countryCode;
});
Step 6 – Duration
Triggered by the loadedmetadata event, this section captures:
- Total video duration
This marks the baseline video state before the user starts interacting with it.
video.addEventListener('loadedmetadata', () => {
session.duration = Math.round(video.duration || 0);
});
Step 7 – Counting a View + Measuring Startup Time
The play and playing events together record:
- When the first “play” action occurred
- How long it took for the first frame to appear (timeToStartMs)
This startup delay metric reflects how quickly the video begins after user intent — critical for measuring QoE (Quality of Experience).
let firstPlayT0 = null;
video.addEventListener('play', () => {
if (session.event !== 'view') session.event = 'view';
if (firstPlayT0 === null) firstPlayT0 = now();
});
video.addEventListener('playing', () => {
if (session.timeToStartMs === null && firstPlayT0 !== null) {
session.timeToStartMs = Math.max(0, Math.round(now() - firstPlayT0));
}
});
Step 8 – Watch Progress (Max Time Watched)
The timeupdate listener continually records the furthest timestamp watched.
This enables the calculation of watch percentage, showing how much of the video the viewer actually watched before leaving.
video.addEventListener('timeupdate', () => {
if (video.currentTime > session.maxTimeWatched) {
session.maxTimeWatched = video.currentTime;
}
});
Step 9 – Completion Tracking
When the ended event fires, the session marks completed = true.
This lets you differentiate partial plays from full completions in your dataset.
video.addEventListener('ended', () => {
session.completed = true;
});
Step 10 – Error Logging
Captures playback errors using the HTML5 video.error object.
Each error is tagged with a code and a friendly name (e.g., MEDIA_ERR_DECODE).
This helps debug whether failures are due to network issues, unsupported formats, or decoding problems.
const MEDIA_ERROR_NAMES = {
1: 'MEDIA_ERR_ABORTED',
2: 'MEDIA_ERR_NETWORK',
3: 'MEDIA_ERR_DECODE',
4: 'MEDIA_ERR_SRC_NOT_SUPPORTED'
};
video.addEventListener('error', () => {
const err = video.error;
if (err && err.code) {
session.errors.push({ code: err.code, name: MEDIA_ERROR_NAMES[err.code] || 'UNKNOWN' });
} else {
session.errors.push({ code: 0, name: 'UNKNOWN' });
}
});
Step 11 – Finalizing the Session
When the viewer leaves or reloads the page, this block:
- Calculates the watchPercent (based on duration and max time watched)
- Pushes the finalized session data into the analyticsRows array
Essentially, it freezes the current session snapshot into a structured record.
let stored = false;
function finalizeSessionAndStore() {
if (stored) return;
stored = true;
// compute watch %
const dur = video.duration || session.duration || 0;
const pct = dur ? Math.min(100, Math.floor((session.maxTimeWatched / dur) * 100)) : 0;
session.watchPercent = pct;
// push the row
analyticsRows.push({
timestamp: session.timestamp,
sessionId: session.sessionId,
videoId: session.videoId,
pageUrl: session.pageUrl,
event: session.event,
completed: session.completed,
watchPercent: session.watchPercent,
durationSec: video.duration,
timeToStartMs: session.timeToStartMs,
device: session.device,
os: session.os,
browser: session.browser,
country: session.country,
countryCode: session.countryCode,
errors: session.errors.map(e => `${e.name}(${e.code})`).join('|')
});
}
Step 12 – Exit Detection and Publishing JSON
The script ensures data isn’t lost:
- It listens for pagehide, beforeunload, and visibilitychange events to call finalizeSessionAndStore() before leaving.
- When the user clicks “Publish Analytics JSON”, it displays the analytics array as formatted JSON in the <pre>block.
This allows anyone to instantly view the full analytics dataset on the page — no servers or APIs needed.
window.addEventListener('pagehide', finalizeSessionAndStore);
window.addEventListener('beforeunload', finalizeSessionAndStore);
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') finalizeSessionAndStore();
});
Step 13 – Publish JSON on the page (button → pretty JSON)
if (publishBtn && out) {
publishBtn.addEventListener('click', () => {
finalizeSessionAndStore();
out.style.display = "block";
out.textContent = JSON.stringify(analyticsRows, null, 2);
});
} else {
console.warn('Missing #publishJson button or #analyticsJson output node.');
}
Example JSON row (what readers should expect)
[{
"timestamp": "2025-10-27T10:50:42.747Z",
"sessionId": "3d99ad46-a0d3-4dab-825b-a7b339b92c13",
"videoId": "livestream-start",
"pageUrl": "https://www.vdocipher.com/blog/?page_id=20104&preview=true",
"event": "view",
"completed": false,
"watchPercent": 85,
"durationSec": 7.36,
"timeToStartMs": 0,
"device": "Desktop",
"os": "macOS",
"browser": "Chrome",
"country": "India",
"countryCode": "IN",
"errors": ""
}]
Optional – JSON to Google Sheet Dashboard
- Upload to Sheets using Extensions → Apps Script with a small script that parses JSON and inserts rows, or simply convert JSON → CSV online and import.
- After sending our data to a Google Sheet, we can later open the sheet to view and pivot the data (or even use Google’s chart features to visualize it). You can make a separate “Dashboard” sheet within the Google Sheet that references these metrics. For example, use formulas to pull average watch % and so on for each video, and present them in a summary table. Use charts (pie charts for device or browser share, bar chart for top countries, line chart over time if you add dates, etc.) to visualize the data. Google Sheets makes it fairly easy to create these once the data is there.
Supercharge Your Business with Videos
At VdoCipher we maintain the strongest content protection for videos. We also deliver the best viewer experience with brand friendly customisations. We'd love to hear from you, and help boost your video streaming business.

My expertise focuses on DRM encryption, CDN technologies, and streamlining marketing campaigns to drive engagement and growth. At VdoCipher, I’ve significantly enhanced digital experiences and contributed to in-depth technical discussions in the eLearning, Media, and Security sectors, showcasing a commitment to innovation and excellence in the digital landscape.

Leave a Reply