Agent skill
Video Analytics
Tracking viewer behavior, engagement, and quality metrics for video content including player events, QoE metrics, watch time, and analytics dashboards.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/other/video-analytics
SKILL.md
Video Analytics
Current Level: Intermediate
Domain: Video Streaming / Analytics
Overview
Video analytics tracks viewer behavior, engagement, and quality metrics. This guide covers player events, QoE metrics, and analytics dashboards for understanding video performance and viewer engagement.
Video Metrics
Core Metrics
// types/analytics.ts
export interface VideoMetrics {
// View metrics
views: number;
uniqueViews: number;
impressions: number;
// Engagement metrics
watchTime: number; // Total seconds watched
averageWatchTime: number;
completionRate: number; // Percentage who finished
engagementRate: number;
// Quality metrics (QoE)
averageBufferTime: number;
averageStartTime: number;
averageBitrate: number;
errorRate: number;
// Interaction metrics
likes: number;
shares: number;
comments: number;
}
Player Events Tracking
// services/video-analytics.service.ts
export class VideoAnalyticsService {
private sessionId: string;
private startTime: number = 0;
private lastPosition: number = 0;
private watchedSegments: Set<number> = new Set();
constructor(private videoId: string, private userId?: string) {
this.sessionId = this.generateSessionId();
}
trackPlay(): void {
this.startTime = Date.now();
this.sendEvent({
event: 'play',
videoId: this.videoId,
sessionId: this.sessionId,
userId: this.userId,
timestamp: Date.now()
});
}
trackPause(position: number): void {
const watchTime = (Date.now() - this.startTime) / 1000;
this.sendEvent({
event: 'pause',
videoId: this.videoId,
sessionId: this.sessionId,
position,
watchTime,
timestamp: Date.now()
});
}
trackProgress(position: number, duration: number): void {
// Track 25%, 50%, 75%, 100% milestones
const percentage = (position / duration) * 100;
const milestones = [25, 50, 75, 100];
milestones.forEach(milestone => {
if (percentage >= milestone && !this.watchedSegments.has(milestone)) {
this.watchedSegments.add(milestone);
this.sendEvent({
event: 'milestone',
videoId: this.videoId,
sessionId: this.sessionId,
milestone,
position,
timestamp: Date.now()
});
}
});
this.lastPosition = position;
}
trackSeek(from: number, to: number): void {
this.sendEvent({
event: 'seek',
videoId: this.videoId,
sessionId: this.sessionId,
from,
to,
timestamp: Date.now()
});
}
trackQualityChange(oldQuality: string, newQuality: string): void {
this.sendEvent({
event: 'quality_change',
videoId: this.videoId,
sessionId: this.sessionId,
oldQuality,
newQuality,
timestamp: Date.now()
});
}
trackBuffering(duration: number): void {
this.sendEvent({
event: 'buffering',
videoId: this.videoId,
sessionId: this.sessionId,
duration,
position: this.lastPosition,
timestamp: Date.now()
});
}
trackError(error: string): void {
this.sendEvent({
event: 'error',
videoId: this.videoId,
sessionId: this.sessionId,
error,
position: this.lastPosition,
timestamp: Date.now()
});
}
trackEnd(): void {
const totalWatchTime = (Date.now() - this.startTime) / 1000;
this.sendEvent({
event: 'end',
videoId: this.videoId,
sessionId: this.sessionId,
watchTime: totalWatchTime,
timestamp: Date.now()
});
}
private async sendEvent(event: any): Promise<void> {
await fetch('/api/analytics/video-event', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(event)
});
}
private generateSessionId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
}
// Usage with Video.js
const analytics = new VideoAnalyticsService(videoId, userId);
player.on('play', () => analytics.trackPlay());
player.on('pause', () => analytics.trackPause(player.currentTime()));
player.on('timeupdate', () => {
analytics.trackProgress(player.currentTime(), player.duration());
});
player.on('seeked', () => {
analytics.trackSeek(previousTime, player.currentTime());
});
player.on('ended', () => analytics.trackEnd());
player.on('error', (e) => analytics.trackError(e.message));
Quality of Experience (QoE) Metrics
// services/qoe-tracker.service.ts
export class QoETrackerService {
private metrics: QoEMetrics = {
startupTime: 0,
bufferingEvents: [],
bitrateChanges: [],
errors: []
};
trackStartupTime(time: number): void {
this.metrics.startupTime = time;
}
trackBuffering(startTime: number, endTime: number): void {
this.metrics.bufferingEvents.push({
startTime,
endTime,
duration: endTime - startTime
});
}
trackBitrateChange(timestamp: number, bitrate: number): void {
this.metrics.bitrateChanges.push({
timestamp,
bitrate
});
}
trackError(error: VideoError): void {
this.metrics.errors.push(error);
}
calculateQoEScore(): number {
let score = 100;
// Penalize long startup time
if (this.metrics.startupTime > 3000) {
score -= 20;
} else if (this.metrics.startupTime > 1000) {
score -= 10;
}
// Penalize buffering
const totalBuffering = this.metrics.bufferingEvents.reduce(
(sum, event) => sum + event.duration,
0
);
score -= Math.min(totalBuffering / 1000 * 5, 30);
// Penalize frequent bitrate changes
if (this.metrics.bitrateChanges.length > 10) {
score -= 15;
}
// Penalize errors
score -= this.metrics.errors.length * 10;
return Math.max(score, 0);
}
getMetrics(): QoEMetrics {
return this.metrics;
}
}
interface QoEMetrics {
startupTime: number;
bufferingEvents: BufferingEvent[];
bitrateChanges: BitrateChange[];
errors: VideoError[];
}
interface BufferingEvent {
startTime: number;
endTime: number;
duration: number;
}
interface BitrateChange {
timestamp: number;
bitrate: number;
}
interface VideoError {
code: string;
message: string;
timestamp: number;
}
Heatmaps
// services/heatmap.service.ts
export class HeatmapService {
async generateHeatmap(videoId: string): Promise<HeatmapData> {
// Get all view events
const events = await db.videoEvent.findMany({
where: {
videoId,
event: { in: ['play', 'pause', 'seek'] }
}
});
// Get video duration
const video = await db.video.findUnique({ where: { id: videoId } });
const duration = video?.duration || 0;
// Create time buckets (1 second intervals)
const buckets = new Array(Math.ceil(duration)).fill(0);
// Count views for each second
events.forEach(event => {
if (event.position !== undefined) {
const bucket = Math.floor(event.position);
if (bucket < buckets.length) {
buckets[bucket]++;
}
}
});
// Normalize to percentage
const maxViews = Math.max(...buckets);
const normalized = buckets.map(count => (count / maxViews) * 100);
return {
videoId,
duration,
data: normalized
};
}
async getDropOffPoints(videoId: string): Promise<DropOffPoint[]> {
const heatmap = await this.generateHeatmap(videoId);
const dropOffs: DropOffPoint[] = [];
// Find significant drops (>20% decrease)
for (let i = 1; i < heatmap.data.length; i++) {
const drop = heatmap.data[i - 1] - heatmap.data[i];
if (drop > 20) {
dropOffs.push({
timestamp: i,
dropPercentage: drop,
viewsBefore: heatmap.data[i - 1],
viewsAfter: heatmap.data[i]
});
}
}
return dropOffs.sort((a, b) => b.dropPercentage - a.dropPercentage);
}
}
interface HeatmapData {
videoId: string;
duration: number;
data: number[]; // Percentage of viewers at each second
}
interface DropOffPoint {
timestamp: number;
dropPercentage: number;
viewsBefore: number;
viewsAfter: number;
}
A/B Testing
// services/video-ab-test.service.ts
export class VideoABTestService {
async assignVariant(userId: string, testId: string): Promise<string> {
// Check if user already assigned
const existing = await db.abTestAssignment.findUnique({
where: {
userId_testId: { userId, testId }
}
});
if (existing) {
return existing.variant;
}
// Assign variant (50/50 split)
const variant = Math.random() < 0.5 ? 'A' : 'B';
await db.abTestAssignment.create({
data: {
userId,
testId,
variant
}
});
return variant;
}
async trackConversion(userId: string, testId: string, metric: string, value: number): Promise<void> {
await db.abTestMetric.create({
data: {
userId,
testId,
metric,
value
}
});
}
async getTestResults(testId: string): Promise<ABTestResults> {
const assignments = await db.abTestAssignment.groupBy({
by: ['variant'],
where: { testId },
_count: true
});
const metrics = await db.abTestMetric.groupBy({
by: ['variant', 'metric'],
where: { testId },
_avg: { value: true }
});
return {
testId,
assignments: assignments.map(a => ({
variant: a.variant,
count: a._count
})),
metrics: metrics.map(m => ({
variant: m.variant,
metric: m.metric,
average: m._avg.value || 0
}))
};
}
}
interface ABTestResults {
testId: string;
assignments: { variant: string; count: number }[];
metrics: { variant: string; metric: string; average: number }[];
}
Analytics Dashboard
// components/AnalyticsDashboard.tsx
export function AnalyticsDashboard({ videoId }: { videoId: string }) {
const [metrics, setMetrics] = useState<VideoMetrics | null>(null);
const [heatmap, setHeatmap] = useState<HeatmapData | null>(null);
useEffect(() => {
loadAnalytics();
}, [videoId]);
const loadAnalytics = async () => {
const [metricsData, heatmapData] = await Promise.all([
fetch(`/api/analytics/video/${videoId}/metrics`).then(r => r.json()),
fetch(`/api/analytics/video/${videoId}/heatmap`).then(r => r.json())
]);
setMetrics(metricsData);
setHeatmap(heatmapData);
};
return (
<div className="analytics-dashboard">
<div className="metrics-grid">
<MetricCard title="Views" value={metrics?.views || 0} />
<MetricCard title="Watch Time" value={formatDuration(metrics?.watchTime || 0)} />
<MetricCard title="Completion Rate" value={`${metrics?.completionRate || 0}%`} />
<MetricCard title="Engagement" value={`${metrics?.engagementRate || 0}%`} />
</div>
{heatmap && (
<div className="heatmap">
<h3>Viewer Retention</h3>
<HeatmapChart data={heatmap} />
</div>
)}
<div className="qoe-metrics">
<h3>Quality of Experience</h3>
<QoEChart videoId={videoId} />
</div>
</div>
);
}
Quick Start
Video Event Tracking
// Track video events
function trackVideoEvent(
videoId: string,
event: 'play' | 'pause' | 'seek' | 'complete',
timestamp: number
) {
analytics.track('video_event', {
videoId,
event,
timestamp,
watchTime: calculateWatchTime(videoId)
})
}
// Calculate watch time
function calculateWatchTime(videoId: string): number {
const events = getVideoEvents(videoId)
let watchTime = 0
let lastPlayTime = 0
events.forEach(event => {
if (event.type === 'play') {
lastPlayTime = event.timestamp
} else if (event.type === 'pause') {
watchTime += event.timestamp - lastPlayTime
}
})
return watchTime
}
Production Checklist
- Event Tracking: Track all video events
- Watch Time: Calculate accurate watch time
- Engagement Metrics: Track engagement (completion rate, etc.)
- Quality Metrics: Track video quality (buffering, bitrate)
- Privacy: Anonymize user data
- Sampling: Use sampling for high traffic
- Real-time: Real-time analytics
- Aggregation: Pre-aggregate metrics
- Dashboards: Analytics dashboards
- Retention: Data retention policies
- Performance: Don't impact playback
- Testing: Test analytics implementation
Anti-patterns
❌ Don't: Track Too Much
// ❌ Bad - Track every millisecond
setInterval(() => {
trackEvent('heartbeat', { timestamp: Date.now() })
}, 1) // Too much data!
// ✅ Good - Track key events
video.addEventListener('play', () => trackEvent('play'))
video.addEventListener('pause', () => trackEvent('pause'))
video.addEventListener('ended', () => trackEvent('complete'))
❌ Don't: No Privacy
// ❌ Bad - Track personal data
trackEvent('video_view', {
userId: user.id,
email: user.email, // Privacy issue!
videoId: video.id
})
// ✅ Good - Anonymize
trackEvent('video_view', {
userId: hashUserId(user.id), // Anonymized
videoId: video.id
})
Integration Points
- Live Streaming (
37-video-streaming/live-streaming/) - Live analytics - Adaptive Bitrate (
37-video-streaming/adaptive-bitrate/) - Quality metrics - Analytics (
23-business-analytics/) - General analytics
Further Reading
Resources
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
agent-ops-spec
Manage specification documents in .agent/specs/. Use when user provides requirements, acceptance criteria, or feature descriptions that need to be tracked and validated against implementation.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-testing
Test strategy, execution, and coverage analysis. Use when designing tests, running test suites, or analyzing test results beyond baseline checks.
agent-ops-state
Maintain .agent state files. Use at session start, after meaningful steps, and before concluding: read/update constitution/memory/focus/issues/baseline consistently.
Didn't find tool you were looking for?