Skip to content

pplayer Player Integration Guide

pplayer is PPCDN's Web player SDK, built on WHEP / WebRTC, with simulcast/ABR, HEVC/H264 multitrack support, P2P-vs-Edge racing, end-to-end latency display, playback buffer tuning, screenshots and recording.

This page is for developers integrating playback into their own pages. To just try it, open the demo above and paste a WHEP URL into the webrtc: field.

1. Capabilities

Component Role
MediaMTXWebRTCReader WHEP protocol, WebRTC connection, RTP receive and SDP negotiation
ABREngine Tracks which quality layers exist, which is playing, and whether the server auto-selects
MMXControlClient WebSocket signaling channel for layer switching with the server
TimeSync Application-layer clock calibration against ppcenter, for end-to-end latency (optional)
codec-capability Detects HEVC decode support and chooses h264/whep vs hevc/whep (optional)
buffer-config Playback buffer (jitter buffer) length (optional)

Adaptive bitrate (ABR) decisions are made server-side; the SDK only records state and applies the server's suggested switch.

2. Quick integration

2.1 Import

The SDK uses ES modules. Drop the files into your static assets and load the entry point with a plain <script type="module"> — no bundler needed:

<script type="module" src="main.js"></script>

ES modules must be served over HTTP(S); opening via file:// is blocked by the browser's CORS policy.

To use a single component instead of the whole page, import it directly:

import { MediaMTXWebRTCReader, MMXControlClient } from './ppplayer.mjs';
import { ABREngine } from './abr-engine.mjs';

2.2 Minimal example

import { MediaMTXWebRTCReader, MMXControlClient } from './ppplayer.mjs';
import { ABREngine } from './abr-engine.mjs';

const abrEngine = new ABREngine();

const reader = new MediaMTXWebRTCReader({
  url: 'https://edge-1.edge.pp-cdn.org/{appId}/{stream}/whep',
  maxBitrate: 2500, // optional: initial bandwidth cap (kbps)

  onTrack: (evt) => {
    const videoEl = document.getElementById('video');
    if (videoEl.srcObject !== evt.streams[0]) {
      videoEl.srcObject = evt.streams[0];
    }
  },
  onConnected: () => initControlClient(reader.sessionId),
  onError: (err) => console.error('playback error:', err),
});

function initControlClient(sessionId) {
  const controlClient = new MMXControlClient(reader.url, sessionId, {
    onTracksInfo: (tracks, activeId) => abrEngine.setTracks(tracks, activeId),
    onLayerSwitched: (id) => abrEngine.notifyLayerSwitched(id),
    onABRMode: (auto) => abrEngine.notifyAutoMode(auto),
    onBandwidthEstimate: (bps) => abrEngine.notifyBandwidthEstimate(bps),
  });
}

You don't need to feed getStats() fps / bitrate into ABR — layer selection is decided server-side from link loss and RTT.

3. Build a play (WHEP) URL from a publish URL

Playback needs the WHEP play URL, not the WHIP publish URL. They differ by one letter; rewrite as follows:

Example
Publish URL (WHIP) https://origin.pp-cdn.org/{appId}/{stream}/whip
Play URL (WHEP) https://edge-1.edge.pp-cdn.org/{appId}/{stream}/whep

Two changes:

  1. host: origin.pp-cdn.org → any Edge domain (from the console or the play-decision API);
  2. suffix: /whip → /whep.

To pin a codec, insert /h264 or /hevc before /whep:

https://edge-1.edge.pp-cdn.org/{appId}/{stream}/hevc/whep

If you paste a whip publish URL into pplayer, it detects it and suggests the whep URL instead.

4. Optional: browser auto-selects HEVC / H264

With multitrack enabled, the server exposes separate h264 and hevc paths. The codec must be chosen once before connecting — it cannot be switched mid-play the way ABR switches layers.

codec-capability detects HEVC decode support (preferring mediaCapabilities.decodingInfo(), falling back to RTCRtpReceiver.getCapabilities('video')) and inserts the chosen codec segment into the WHEP URL:

.../{stream}/whep  →  .../{stream}/hevc/whep   (HEVC supported)
.../{stream}/whep  →  .../{stream}/h264/whep   (unsupported or module not imported)
  • If the URL already contains /h264/whep or /hevc/whep, detection is skipped.
  • You can force it with the URL param ?codecType=hevc.
  • If HEVC is chosen but the connection fails with a codec/SDP-looking reason, it automatically downgrades to H264 and reconnects once within the same playback.

5. Optional: playback buffer (latency vs jitter tolerance)

The buffer decides how many milliseconds of media to cache before rendering: larger tolerates more jitter but adds latency; smaller lowers latency but stutters more easily. It is purely local, not negotiated with the server.

  • Default 200 ms, usable range 100–1000 ms;
  • URL param ?bufferMs=300 presets it; the UI slider overrides live;
  • Prefers RTCRtpReceiver.jitterBufferTarget, falling back to Chrome's playoutDelayHint.
import { applyPlayoutBuffer } from './buffer-config.mjs';
const applied = applyPlayoutBuffer(pc, 300); // returns the API actually applied, or null

6. Optional: end-to-end latency (TimeSync)

ppobs embeds UTC-calibrated timestamps in the bitstream. To compute "end-to-end latency = calibrated local now − in-frame timestamp", the player must first align its clock to UTC — using an uncalibrated Date.now() gives wrong results.

TimeSync performs one application-layer clock-offset estimate against ppcenter (NTP's four-timestamp algorithm, several rounds, keeping the one with the lowest RTT; re-calibrates every 45 s by default):

import { TimeSync } from './time-sync.mjs';

const timeSync = new TimeSync({ ppcenter: 'https://api.pp-cdn.org' });
timeSync.start().catch((e) => console.warn('calibration unavailable:', e.message));

const now = timeSync.now(); // null until calibration completes
if (now !== null) {
  const delayMs = now - embeddedTimestamp;
}

Key convention: now() returns null before calibration completes; treat that as "latency not computable yet" and do not fall back to a raw Date.now(). Without the module or when ppcenter is unreachable, playback is unaffected and the latency display falls back to an estimate.

TimeSync can also report latency via reportLatency(path, delayMs), where path is 'edge' or 'p2p'.

7. P2P acceleration and Edge/P2P racing

When enabled, the player simultaneously opens both connections and uses whichever produces a frame first:

  • Edge path: normal WHEP to an edge node;
  • P2P path: direct to the publisher, signaled through ppcenter.

The winner is the first decodable video frame, not ICE connected. NAT traversal has no public-Internet success guarantee (symmetric NAT, CGNAT often fail); racing reduces the "try P2P, wait for failure, then Edge" delay to zero.

To enable, the URL must carry all five parameters (missing any is a clear error, not a silent downgrade):

index.html?ppcenter=https://api.pp-cdn.org&appId=<appId>&streamName=<stream>&txTime=<hex>&txSecret=<hmac>

Flow: NAT probe and report → request a play decision from ppcenter → it returns edge-only (WHEP only) or p2p-connect (plus P2P session params). Whether P2P is used is decided entirely by the server.

Without these parameters, the player uses the WHEP URL in the input box and the P2P code never runs — this is edge-only integration.

txTime / txSecret are derived from appSecret; never put appSecret into a play page or share link. To issue short-lived tokens to players, have your own server call ppcenter's open API.

8. Screenshot & recording

The control bar offers 📷 screenshot and ⏺ record buttons; the logic lives in main.js:

  • Screenshot: captures the current frame with <canvas> and downloads a PNG;
  • Record: records the picture to WebM with MediaRecorder, up to 60 s, stoppable early;
  • Both are disabled in ABR Auto mode: resolution/bitrate switching on the fly would make captures jump, so pick a quality layer manually first.

9. File list

File Role
main.js Entry: UI binding, playback orchestration
ppplayer.mjs MediaMTXWebRTCReader + MMXControlClient
abr-engine.mjs Layer state tracking
time-sync.mjs Clock calibration
obs-timestamp.mjs End-to-end latency computation
sei-timestamp.mjs In-bitstream SEI timestamp parsing (Chromium)
codec-capability.mjs HEVC/H264 capability detection
buffer-config.mjs Playback buffer length
play-request.mjs Requests a play decision from ppcenter
nat-probe.mjs NAT type probe
playback-paths.mjs The Edge / P2P playback paths
playback-race-controller.mjs Racing state machine
play-decision-runner.mjs Assembles the race from the decision

10. API reference

MediaMTXWebRTCReader(config)

Param Type Required Notes
url String yes Full WHEP URL
maxBitrate Number no Initial max bandwidth (kbps) for SDP b=AS
user / pass String no Basic Auth
token String no Bearer token
onTrack / onConnected / onError Function no Media track / connected / error callbacks

Properties sessionId (read-only), pc (the underlying RTCPeerConnection); method close().

MMXControlClient(whepUrl, sessionId, callbacks)

Callbacks: onConnected, onDisconnected, onTracksInfo(tracks, activeId), onLayerSwitched(id), onABRMode(auto), onBandwidthEstimate(bps), onAbrRecommend(targetTrackId).

Methods:

  • selectLayer(trackId, reason): request a layer switch. Use the exported constant ABR_REASON_AUTO_BANDWIDTH for reason to mean "apply the server suggestion" (stays in Auto); any other value means the user picked manually.
  • setABRMode(auto): hand layer selection back to the server (true) or take it (false).
  • close(): close the connection.

ABREngine

Properties isAutoMode, currentTrackId, lastBandwidthEstimate; methods setTracks, notifyLayerSwitched, notifyManualSwitch, notifyAutoMode, setAutoMode, selectedTrackId().

11. FAQ

Q: Blank page / CORS error? ES modules must be loaded over HTTP(S); file:// won't work.

Q: Safari / iOS doesn't autoplay? Autoplay policy requires one user gesture to start playback.

Q: The latency display is empty? The clock is not calibrated yet (TimeSync.now() returns null); wait for calibration to complete.

Q: P2P didn't connect? Under symmetric NAT / CGNAT, no direct P2P is normal; the player falls back to Edge automatically and viewing is unaffected.