Serial playback, step by step

For courses, compliance training and anything else that must be watched in order: with serial playback on, no viewer can reach a moment of a video sooner than that much time after they started watching it. This page takes you from switching it on to reading each learner's progress. Every rule and field is in the reference: Serial playback and Playback sessions and progress.

You need: the Growth or Enterprise plan, a video library, and your own backend running one of plycdn's server packages (ASP.NET Core, Node.js or Python) or calling the API with your key.

1How it works

  1. You switch serial playback on for a library.
  2. Your server tells plycdn who is watching, with an id of your own for that viewer: the resumeId.
  3. plycdn starts a playback session for that viewer and keeps their position. It lets them watch up to the time that has really passed since they started, across every visit with the same resumeId.
  4. Your server reads each viewer's progress from plycdn when you need it: to unlock the next lesson, to issue a certificate, or to start someone over for a retake.

The browser never names the viewer, and nothing it does (an extension, a changed player, a copied link) moves anyone ahead. Viewers can still go back to anything they have watched, pause, and change quality.

2Choose your resumeId

The resumeId is your own id for a viewer: a learner, a seat or an enrolment. Use the same id on every visit, from every device, so progress follows the person.

  • 1 to 128 characters of A-Z a-z 0-9 . _ : @ -.
  • Never an email address or a name: plycdn doesn't need to know who the person is.
  • One id per person per course if a learner may take the same video in two courses with separate progress, such as learner-42:course-7.
  • A different id starts from zero. Keep the one you chose.

3Switch it on

In the dashboard: Video streaming → Video libraries → New library, and turn on Serial playback. For a library you already have: open it → Settings → Serial playback → Viewers can't skip ahead, then save.

From your code: send "playback": {"serial": true} when you create the library, or later:

PATCH /api/content/v1/libraries/{libraryId}     {"playback": {"serial": true}}

Turning it on can answer 429 rate_limited or 409 serial_playback_busy: wait for the Retry-After and try again. A library made before plycdn links answers 409 serial_playback_needs_plycdn_links: contact support and we will move it, or create a new library and add its videos there. Without the feature in your plan, the answer is 403 not_included_in_plan.

Switching serial playback on or off sends a signing_profile.changed webhook (reason serial_changed, Signing settings changed in the dashboard), so servers that receive it with the packages stop or resume signing that library's videos themselves at once (Keep your signers current).

4Name the viewer on your server

With plycdn's server packages this is the only code you add. The players already ask your backend's sign route for a link; once the route knows the viewer's resumeId, it starts a session for them by itself.

ASP.NET Core

builder.Services.AddPlycdnVideo(builder.Configuration);
builder.Services.AddPlycdnStorage(o =>
    o.PlaybackResumeIdFor = (http, library, video) => http.User.FindFirst("learner")?.Value);

Node.js

app.use('/api/plycdn/v1', createPlycdnRouter({
  storage,
  signer: new PlycdnLinkSigner(storage),
  video: new PlycdnVideoApi(storage),
  resolveOrganization: (req) => req.user?.plycdnOrganizationId ?? null,
  authorize: (req, operation, bucketId, key) => true,   // your own rules
  playbackResumeIdFor: (req, libraryId, videoId) => req.user?.learnerId ?? null,
}));

Python

def playback_resume_id_for(request, library_id, video_id):
    user = getattr(request.state, "user", None)
    return user.learner_id if user else None

app.include_router(plycdn_router(
    storage=storage, signer=AsyncPlycdnLinkSigner(storage), video=AsyncPlycdnVideoApi(storage),
    resolve_organization=resolve_organization, authorize=authorize,
    playback_resume_id_for=playback_resume_id_for,
), prefix="/api/plycdn/v1")

Return nothing (null or None) for a visitor you can't name. They still get a session, but no progress is kept for them.

5Play the video

Nothing changes in your frontend. plycdn's players handle serial playback by themselves: they need no prop and no code.

React

import { PlycdnProvider, PlycdnPlayer } from '@plycdn/react/video';

<PlycdnProvider config={{ baseUrl: '/api/plycdn/v1' }}>
  <PlycdnPlayer libraryId={libraryId} videoId={videoId} />
</PlycdnProvider>

Angular

<plycdn-player [libraryId]="libraryId" [videoId]="videoId"></plycdn-player>

The players save the viewer's position every minute while playing, on pause, at the end and when the page is hidden, and start a new session before the old one ends. When the viewer comes back, playback resumes where plycdn last recorded them.

With a player of your own

Start the session from your server and give its url to any video player, the ones built into phones included:

POST /api/content/v1/playback-sessions   {"bucketId": "<library id>", "videoId": "<video id>", "resumeId": "learner-42"}

The server packages call it StartPlaybackAsync, startPlayback and start_playback. The answer carries url, serial: true, watchedSeconds, durationSeconds, completed, pass and progressUrl. Then:

  • Add crossorigin to your own video element (<video crossorigin controls>) before you give it the address.
  • Post the position to progressUrl as text/plain, every minute and on pause: {"pass": "<pass>", "position": <seconds>}. plycdn keeps no more than the time that has really passed, so a claimed position never moves anyone ahead.
  • Start a new session before expiresAt. It carries on from the saved progress.

The same call on a library without serial playback answers ordinary links with serial: false, so one call works for every library.

6Read and use progress

Read progress from your server, with your key, whenever you need it:

What you want API ASP.NET Core Node.js Python
One viewer, one video GET /playback-progress?bucketId=…&videoId=…&resumeId=… GetPlaybackProgressAsync getPlaybackProgress get_playback_progress
One viewer, every video they started GET /playback-progress?bucketId=…&resumeId=… ListPlaybackProgressAsync listPlaybackProgress list_playback_progress
Start a viewer over (a retake) DELETE /playback-progress?bucketId=…&videoId=…&resumeId=… ResetPlaybackProgressAsync resetPlaybackProgress reset_playback_progress
Watch time per viewer or per video GET /libraries/{libraryId}/watch-time GetWatchTimeReportAsync getWatchTimeReport get_watch_time_report

Each answer carries watchedSeconds, durationSeconds, completed, completedAt, updatedAt, watchTimeSeconds (the time the video was really playing) and firstWatchedAt. The watch time report works the same in libraries without serial playback: see Watch time per viewer. A video counts as complete within its last six seconds. Before a viewer starts a video the answer is 404 progress_not_found: treat it as "not started".

Unlock the next lesson when completed is true:

const p = await video.getPlaybackProgress(orgId, libraryId, lessonVideoId, learner.id).catch(() => null);
const unlocked = p?.completed === true;

Use these figures, never the browser's, for anything you enforce. The players' own onProgress (React) and (progress) (Angular) events come from the browser: they are fine for a progress bar, not for a certificate.

In the dashboard: open a video → Viewer progress, enter the viewer's id, and Look up or Start over.

7Check it works

  1. Turn serial playback on for a test library and add a short video.
  2. Sign in to your application as a test learner and play it for a minute.
  3. Try to jump ahead: the player stops at the point you have reached.
  4. In the dashboard, open the video → Viewer progress, and look up your test learner's id: about a minute watched.
  5. Close the tab, open it again: playback resumes where you stopped.

8Questions and problems

My signing call answers serial_playback_requires_session. Signing a video yourself isn't allowed in a serial library. Start a session instead (StartPlaybackAsync, startPlayback, start_playback), or let the players' sign route do it.

Progress stays at zero. Your resumeId hook returned nothing, so the session keeps no progress. Check that the viewer is signed in when the player asks for its link.

Progress isn't shared across devices. The two devices sent different resumeIds. Use the same id for the person everywhere.

Does a paused or forgotten tab count? Yes. Time paused, or a tab left playing, counts as time watched. Serial playback can't tell whether someone is looking at the screen, and it can't stop screen recording. Pair it with attention monitoring if you need proof that someone watched.

Can viewers download the video or use the scrubbing previews? No, not in a session. A live stream's recording can't be played in a serial library (serial_playback_unavailable).

Can I turn it off later? Yes: switch it off in the library's settings, or send "playback": {"serial": false}.