July 23, 2026

Track whether a user actually watched a video, and how far they got

Shashank Ramineni
Shashank Ramineni
VP - Sales & Marketing

Marking a lesson complete when someone clicks play is a lie your learners will happily exploit. The honest questions are whether they actually watched it, how far they got, and whether a completion is real or a drag to the end. Your view data answers all three, once you tie each view to a name.

This article turns raw view data into a per-learner progress table: who started each lesson, how far they reached, how much of the runtime they actually watched, and who earned credit versus who needs a second look.

Here's the tracker you'll build, from the sample dataset below. Sophie watched a lesson end to end and earned credit. Emma reached the end of another but watched 15% of its runtime, so it is flagged for review, not failed. Jack opened one nine times and watched twelve seconds.

How the data flows

Views come from FastPix with a stable viewer ID and, when the learner is logged in, your own user ID. Names come from your course roster. The join is the article.

Sample dataset:

https://drive.google.com/file/d/1iMRkZEryzGc0_7GdAc_eTzd4Xluz-ZCu/view?usp=sharing
https://drive.google.com/file/d/1XzMtleO6oyhFRTdxD_wHEcXF-uRdL5s1/view?usp=sharing

Download the sample datasetfastpix-video-data-sample-export.csv and course-learner-roster.csv 80 real (anonymized) views from an online course, plus a sample roster mapping viewers to learner names. Every snippet below runs against them. No FastPix account yet? The sample works without one - or start free with $25 credit (no credit card) and track your own learners.

Part 1: Get view data flowing from your player

Playback happens in your player. FastPix Video Data captures every interaction from inside it: it dedupes the beacons, stitches events into sessions, adds geo and device data, and computes quality scores, across millions of concurrent views when you need it. What you export is one clean row per view, with a viewer ID on every row.

Already collecting views with FastPix? Skip to Part 2. New here? Setup is about ten minutes.

Step 1: Add the player (or add monitoring to the one you have)

html
<script src="https://unpkg.com/@fastpix/[email protected]/dist/player.js"></script>

<fastpix-player playback-id="YOUR_PLAYBACK_ID" stream-type="on-demand"></fastpix-player>

Own player? The monitoring SDK captures the same data from hls.js, video.js, dash.js, and native mobile players:

javascript
import fastpixMetrix from "@fastpix/data-core";

fastpixMetrix.tracker(videoElement, {
  hlsjs: hls,
  Hls: Hls,          // both are required
  data: { workspace_id: "YOUR_WORKSPACE_KEY" },
});

Step 2: Pass your user ID, so views have a name to join to

Every view already carries a FastPix viewer_id, stable per browser, which tracks an anonymous learner across sessions. To turn that into a name, pass your own user ID in a custom dimension whenever the learner is logged in:

javascript
fastpixMetrix.tracker(videoElement, {
  hlsjs: hls,
  Hls: Hls,
  data: {
    workspace_id: "YOUR_WORKSPACE_KEY",
    custom_2: `${user.id}::${user.email}`,  // your user ID  <- the join key
    custom_5: lesson.slug,                  // which lesson
    custom_9: course.title,                 // which course
  },
});

The custom dimension is the reliable join key: it follows the learner across devices. The viewer_id is the fallback for views logged before a learner signs in. Pass identity on every authenticated play and your coverage climbs; skip it and your learners stay anonymous.

Step 3: Export the views

Export from the Video Data dashboard (Views → Export CSV) or the export API. The columns this article needs: final_viewer_id, custom_2 (your user ID), video_title, video_duration, view_max_playhead_position (how far they reached), and view_total_content_playback_time (how long they actually watched).

Part 2: Turn views into learner progress

The stack is chdb (embedded ClickHouse, pip install chdb). Same SQL later runs on a real ClickHouse.

Step 4: Measure reach and coverage per learner

Two numbers, not one. Reach is how far the playhead got, view_max_playhead_position / duration. Coverage is how much of the runtime they actually watched, view_total_content_playback_time / duration. A learner can reach 100% by dragging the bar and still have watched almost nothing, so you need both.

python
import chdb

vl = chdb.query("""
    SELECT final_viewer_id AS viewer,
           video_title      AS lesson,
           count()          AS opens,
           any(CAST(video_duration AS Int64))                   AS dur_ms,
           max(CAST(view_max_playhead_position AS Int64))       AS reach_ms,
           sum(CAST(view_total_content_playback_time AS Int64)) AS watch_ms
    FROM file('fastpix-video-data-sample-export.csv', CSVWithNames)
    WHERE video_duration > 0
    GROUP BY viewer, lesson
""", "DataFrame")

vl["reach"]    = (vl["reach_ms"] / vl["dur_ms"]).clip(upper=1.0)   # how far the bar got
vl["coverage"] =  vl["watch_ms"] / vl["dur_ms"]                     # how much they actually watched

Completion is judged on the lesson each learner got furthest in, so roll up to one row per learner and keep that lesson's reach and coverage.

Step 5: Join to your roster for names

FastPix gives you the viewer ID; your roster gives you the name. The sample ships a small course-learner-roster.csv standing in for your LMS user table:

python
import csv
roster = {row["viewer_id"]: row["name"]
          for row in csv.DictReader(open("course-learner-roster.csv"))}

In your own app you'd key this join on the user ID you passed in custom_2, and the roster would be a JOIN against your users table rather than a CSV. The shape is identical.

Start using FastPix for free - $25 credit, no credit card required. Turn your own play data into named learner progress this week. Create your workspace →

Step 6: Decide credit, and flag skips for review

One rule turns reach and coverage into a status a course team can defend. Credit needs both: reach the end of the lesson and watch enough of its runtime. Reaching the end on far less watch time is flagged for review, not failed.

python
PASS_REACH, COV_FLOOR = 0.90, 0.60   # reach 90% AND watch 60% of runtime for credit

def verdict(r):
    if r.watch_min < 1.0:                             return "Opened, didn't watch"
    if r.reach >= PASS_REACH and r.coverage >= COV_FLOOR:  return "Completed"
    if r.reach >= PASS_REACH and r.coverage <  COV_FLOOR:  return f"Reached end, watched {r.coverage:.0%}"
    if r.reach >= 0.5 or r.watch_min >= 5:             return "In progress"
    return "Just started"

Why review and not fail: coverage under the floor is a signal, not proof. A learner watching at 1.5x speed, or one who rewatched half and skipped the intro, can legitimately land below 100%. The flag says "reached the end on light watch time, look before you certify," never "they cheated." Render the table (full template in the notebook) and you get the tracker at the top.

Part 3: What the sample tracker shows

Genuine completion looks different from reaching the end. Sophie Turner and Daniel Hayes each watched a lesson through, 100% reached and 100% of the runtime watched, so they earn credit outright. Emma Carter reached 98% of a longer lesson but watched only 15% of its runtime: she got to the end, on eight minutes of a fifty-five-minute video. Under a "reached 90% = complete" rule she passes; the coverage column holds her for review instead.

Flag for review, do not accuse. Emma's 15% could be a fast-forward through material she already knew, playback at higher speed, or genuine skimming. The data cannot tell you which, and the tracker does not pretend to. It surfaces the gap and leaves the judgment to a human, which is the only honest thing view data can do here.

Opens are not views. Jack Thompson opened lessons nine times and watched twelve seconds total; Liam Walsh opened four times and watched under twenty seconds. A completion checkbox tied to "pressed play" would mark both as done. Watch time earns them no credit.

Coverage exposes skimming even below the cutoff. Ava Sullivan reached 67% but watched 13% of the runtime; Ethan Brooks reached 64% on 18%. Neither is near the credit cutoff, so neither is flagged, but the reach-versus-coverage gap already tells a course team these learners are jumping ahead, not absorbing.

For compliance and mandatory training

If a completion carries legal or certification weight, "reached the end" is the wrong test and everyone in L&D knows it. An employee who drags the safety-training bar to 100% has a completion record and has learned nothing, and that gap is exactly what an auditor asks about. The coverage gate is the defensible signal: credit requires watching the runtime, not touching the end of the progress bar. Set COV_FLOOR to match how strict the certification needs to be, keep the reach-and-coverage numbers per attempt as your audit trail, and hold anything flagged for review before you issue a certificate.

Part 4: Act on the progress data

FastPix supplies reach, coverage, and watch time; your app decides what each verdict triggers.

python
for r in learners:
    if r.reach >= 0.9 and r.coverage >= 0.6:
        issue_certificate(r.user_id)              # genuine completion
    elif r.reach >= 0.9 and r.coverage < 0.6:
        hold_for_review(r.user_id, r.coverage)    # reached end, light watch time
    elif r.watch_min < 1.0 and r.opens >= 3:
        remind(r.user_id, resume_link(r))         # opened repeatedly, never watched

The same rows drive the workflows a learning product needs:

SignalConditionWhat you trigger
Genuine completionReached 90% and watched 60%+ of runtimeIssue certificate, unlock next lesson
Reached end, light watchReached 90% but under the coverage floorHold certificate, flag for instructor review
Opened, never watched3+ opens, under 1 min watchedReminder with a resume link
Stalled mid-courseNo new progress in 7 daysCheck-in email or instructor flag

The workflow cookbook covering these end to end is coming as a follow-up.

How different teams use this

TeamWhat they trackThe number that matters
Course platform (education)Per-learner lesson progress and genuine completioncoverage at the cutoff
Corporate L&D / complianceProvable completion of mandatory trainingcoverage as the audit signal
B2B SaaS with video onboardingWhich users actually finished onboardingreach and coverage together

Running this on your own data

Point Step 4 at your own export and swap the roster for a join against your users table on the ID you pass in custom_2. Two dials to set for your catalog: COV_FLOOR (higher for compliance, lower for casual content) and how you treat faster-than-1x playback, which reads as lower coverage and may deserve its own allowance. And watch your identity coverage as you roll out: every authenticated play that carries a user ID is a row that shows up named; the rest stay anonymous.

Frequently Asked Questions (FAQs)

How do I track if a user watched a video?

Measure both the furthest playback position reached and the total amount of time the viewer actually watched. A learner who reaches the end of the video but accumulates very little watch time likely skipped through the content rather than genuinely watching it.

How do I stop learners skipping to the end to mark a lesson complete?

Base completion on viewing coverage instead of playback position alone. Award completion only when the learner has watched a sufficient percentage of the video's runtime. Learners who jump directly to the end with minimal watch time can be flagged for review instead of receiving credit.

Can I tell if someone watched at 2x speed or skipped?

You can detect that the recorded watch time is shorter than the video's total runtime, but you cannot determine the exact reason. Faster playback, intentional skipping, and selective rewatching can all produce similar results, so treat low watch coverage as a review signal rather than definitive proof.

When should a lesson count as complete?

A lesson should be considered complete only when the learner reaches approximately 90% of the video and watches enough of the runtime to meaningfully consume the content. Using both playback progress and watch time makes completion much harder to manipulate.

Why are most of my viewers anonymous?

Viewer identity is only available when your application passes a user ID as a custom dimension during authenticated playback. Views that occur before sign-in, or sessions where no user identifier is provided, are recorded using anonymous viewer IDs instead.

What does it cost to start?

Getting started is free. You receive a $25 credit with no credit card required. The Video Data free tier includes 100,000 plays per month, and the FastPix Player is free for FastPix customers.

Share

Stay Ahead of Video
Streaming Trends

Start shipping video today.