Home
Video Engagement Tracker
📹

Video Engagement Tracker

YouTube Analytics Integration

Advanced YouTube video engagement tracking with milestone detection (25%, 50%, 75%, 100%), play/pause events, and detailed analytics integration for comprehensive video performance measurement.

EngagementAdvancedEngagement Tracking

Key Features

YouTube API integration
Milestone tracking (25%, 50%, 75%, 100%)
Play/pause event detection
Video completion analytics
Multiple video support
Custom event configuration

Script Overview

📖

What It Does

This script integrates with YouTube's Player API to track detailed video engagement metrics including play/pause events, seek behaviors, and milestone completions. Perfect for analyzing how users interact with your product demos and educational content.

🎯

Use Case

Perfect for SaaS companies with product demo videos or educational content. Essential for understanding which parts of your videos drive engagement and conversions.

Key Benefits

  • Detailed video engagement analytics
  • Milestone-based conversion tracking
  • User interaction behavior insights
  • Demo effectiveness measurement
  • Content optimization data
  • Enhanced user journey tracking

JavaScript Code

<script>
// Video Engagement Tracker Script
// Version: 1.0 - YouTube Integration
// Last Updated: 2025-09-09

(function() {
  'use strict';
  
  var config = {
    // Milestone percentages to track
    milestones: [25, 50, 75, 100],
    
    // Events to track
    events: {
      videoStart: 'video_start',
      videoPause: 'video_pause',
      videoResume: 'video_resume',
      videoComplete: 'video_complete',
      videoMilestone: 'video_milestone',
      videoSeek: 'video_seek'
    },
    
    // Minimum watch time before counting as "started" (seconds)
    minWatchTime: 3,
    
    debug: false
  };

  var trackedVideos = {};
  var ytReady = false;

  function debugLog(message, data) {
    if (config.debug) {
      console.log('[Video Tracker] ' + message, data || '');
    }
  }

  function initYouTubeAPI() {
    if (window.YT && window.YT.Player) {
      ytReady = true;
      initVideoTracking();
      return;
    }

    // Load YouTube API if not already loaded
    if (!window.onYouTubeIframeAPIReady) {
      var tag = document.createElement('script');
      tag.src = 'https://www.youtube.com/iframe_api';
      var firstScriptTag = document.getElementsByTagName('script')[0];
      firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);

      window.onYouTubeIframeAPIReady = function() {
        ytReady = true;
        initVideoTracking();
      };
    }
  }

  function getVideoData(player, iframe) {
    try {
      var videoUrl = player.getVideoUrl && player.getVideoUrl();
      var videoId = player.getVideoData && player.getVideoData().video_id;
      var title = player.getVideoData && player.getVideoData().title;
      
      return {
        videoId: videoId || 'unknown',
        title: title || 'Unknown Video',
        url: videoUrl || iframe.src,
        duration: player.getDuration && player.getDuration() || 0,
        iframe: iframe
      };
    } catch (e) {
      debugLog('Error getting video data:', e);
      return {
        videoId: 'unknown',
        title: 'Unknown Video',
        url: iframe.src || 'unknown',
        duration: 0,
        iframe: iframe
      };
    }
  }

  function trackVideoEvent(eventName, videoData, additionalData) {
    window.dataLayer = window.dataLayer || [];
    
    var eventData = {
      event: eventName,
      video_id: videoData.videoId,
      video_title: videoData.title,
      video_url: videoData.url,
      video_duration: videoData.duration,
      video_current_time: additionalData && additionalData.currentTime || 0,
      video_percent_watched: additionalData && additionalData.percentWatched || 0
    };

    if (additionalData) {
      Object.keys(additionalData).forEach(function(key) {
        if (key !== 'currentTime' && key !== 'percentWatched') {
          eventData['video_' + key] = additionalData[key];
        }
      });
    }

    window.dataLayer.push(eventData);
    debugLog('Video event tracked:', eventData);
  }

  function setupVideoPlayer(iframe) {
    try {
      var player = new YT.Player(iframe, {
        events: {
          onStateChange: function(event) {
            handleStateChange(event, iframe);
          },
          onReady: function(event) {
            var videoData = getVideoData(event.target, iframe);
            trackedVideos[iframe.id] = {
              player: event.target,
              data: videoData,
              startTime: null,
              milestones: {},
              totalWatchTime: 0,
              interactions: 0
            };
            debugLog('Video player ready:', videoData);
          }
        }
      });
    } catch (e) {
      debugLog('Error setting up video player:', e);
    }
  }

  function handleStateChange(event, iframe) {
    var videoInfo = trackedVideos[iframe.id];
    if (!videoInfo) return;

    var player = event.target;
    var currentTime = player.getCurrentTime && player.getCurrentTime() || 0;
    var duration = player.getDuration && player.getDuration() || 0;
    var percentWatched = duration > 0 ? Math.round((currentTime / duration) * 100) : 0;

    switch (event.data) {
      case YT.PlayerState.PLAYING:
        if (!videoInfo.startTime && currentTime > config.minWatchTime) {
          videoInfo.startTime = Date.now();
          trackVideoEvent(config.events.videoStart, videoInfo.data, {
            currentTime: currentTime,
            percentWatched: percentWatched
          });
        } else if (videoInfo.startTime) {
          trackVideoEvent(config.events.videoResume, videoInfo.data, {
            currentTime: currentTime,
            percentWatched: percentWatched
          });
        }
        
        // Start milestone tracking
        startMilestoneTracking(iframe.id);
        break;

      case YT.PlayerState.PAUSED:
        if (videoInfo.startTime) {
          trackVideoEvent(config.events.videoPause, videoInfo.data, {
            currentTime: currentTime,
            percentWatched: percentWatched
          });
        }
        stopMilestoneTracking(iframe.id);
        break;

      case YT.PlayerState.ENDED:
        trackVideoEvent(config.events.videoComplete, videoInfo.data, {
          currentTime: currentTime,
          percentWatched: 100,
          totalWatchTime: videoInfo.totalWatchTime
        });
        stopMilestoneTracking(iframe.id);
        break;
    }
  }

  function startMilestoneTracking(videoId) {
    var videoInfo = trackedVideos[videoId];
    if (!videoInfo || videoInfo.milestoneInterval) return;

    videoInfo.milestoneInterval = setInterval(function() {
      checkMilestones(videoId);
    }, 1000);
  }

  function stopMilestoneTracking(videoId) {
    var videoInfo = trackedVideos[videoId];
    if (videoInfo && videoInfo.milestoneInterval) {
      clearInterval(videoInfo.milestoneInterval);
      videoInfo.milestoneInterval = null;
    }
  }

  function checkMilestones(videoId) {
    var videoInfo = trackedVideos[videoId];
    if (!videoInfo) return;

    try {
      var currentTime = videoInfo.player.getCurrentTime();
      var duration = videoInfo.player.getDuration();
      var percentWatched = duration > 0 ? Math.round((currentTime / duration) * 100) : 0;

      config.milestones.forEach(function(milestone) {
        if (percentWatched >= milestone && !videoInfo.milestones[milestone]) {
          videoInfo.milestones[milestone] = true;
          trackVideoEvent(config.events.videoMilestone, videoInfo.data, {
            currentTime: currentTime,
            percentWatched: percentWatched,
            milestone: milestone
          });
        }
      });
    } catch (e) {
      debugLog('Error checking milestones:', e);
    }
  }

  function initVideoTracking() {
    if (!ytReady) return;

    var iframes = document.querySelectorAll('iframe[src*="youtube.com"], iframe[src*="youtu.be"]');
    
    Array.prototype.forEach.call(iframes, function(iframe, index) {
      if (!iframe.id) {
        iframe.id = 'yt-player-' + index;
      }
      
      // Ensure iframe has required parameters for API
      var src = iframe.src;
      if (src.indexOf('enablejsapi=1') === -1) {
        src += (src.indexOf('?') > -1 ? '&' : '?') + 'enablejsapi=1';
        iframe.src = src;
      }
      
      setupVideoPlayer(iframe);
    });

    debugLog('Initialized tracking for ' + iframes.length + ' YouTube videos');
  }

  // Initialize
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', initYouTubeAPI);
  } else {
    initYouTubeAPI();
  }

  // Expose utility functions
  window.videoEngagementTracker = {
    getTrackedVideos: function() {
      return trackedVideos;
    },
    reinitialize: function() {
      initVideoTracking();
    }
  };

})();
</script>

💡 Pro Tip: Copy the entire code block above and paste it directly into a GTM Custom HTML tag. The script is self-contained and ready to use immediately.