Agent skill

Video Analytics

Tracking viewer behavior, engagement, and quality metrics for video content including player events, QoE metrics, watch time, and analytics dashboards.

Stars 163
Forks 31

Install this agent skill to your Project

npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/data/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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// 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

typescript
// ❌ Bad - Track every millisecond
setInterval(() => {
  trackEvent('heartbeat', { timestamp: Date.now() })
}, 1)  // Too much data!
typescript
// ✅ Good - Track key events
video.addEventListener('play', () => trackEvent('play'))
video.addEventListener('pause', () => trackEvent('pause'))
video.addEventListener('ended', () => trackEvent('complete'))

❌ Don't: No Privacy

typescript
// ❌ Bad - Track personal data
trackEvent('video_view', {
  userId: user.id,
  email: user.email,  // Privacy issue!
  videoId: video.id
})
typescript
// ✅ 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

Didn't find tool you were looking for?

Be as detailed as possible for better results