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.
- Online demo: https://pplayer.pp-cdn.org/
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:
- host:
origin.pp-cdn.org→ any Edge domain (from the console or the play-decision API); - 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
whippublish URL into pplayer, it detects it and suggests thewhepURL 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/whepor/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=300presets it; the UI slider overrides live; - Prefers
RTCRtpReceiver.jitterBufferTarget, falling back to Chrome'splayoutDelayHint.
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/txSecretare derived fromappSecret; never putappSecretinto 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 constantABR_REASON_AUTO_BANDWIDTHforreasonto 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.