Proof of Play
Proof of Play provides real-time evidence of video content playback across all layers in a Disguise session. It tracks which clips are playing on which layers, how many frames have been played, and maintains a rolling history of recently completed playback events - the same data that Designer can export as a CSV report, but accessible live via the Live Update API.
This is primarily useful for fixed installation projects that need to prove content playback to a content management system (CMS) or third-party reporting platform without waiting for a session to end.
For an overview of Proof of Play in Designer, see the Proof of Play user guide.
Proof of Play requires an active Designer session. It is not available via the Service API.
Overview
Proof of Play data is exposed via the ProofOfPlaySubsystem on the Live Update WebSocket. Two streams of data are available:
- Active records - all clips currently playing, updated in real time as frames are played
- Recent records - a rolling history of the most recently completed playback events (up to 300)
Connecting
Open a WebSocket connection to the Live Update endpoint on the Director machine:
const DISGUISE_IP = "director";
const socket = new WebSocket(`ws://${DISGUISE_IP}:80/api/session/liveupdate`);
socket.onopen = () => {
console.log("Connected");
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
// handle messages here
}; import json
from websockets.sync.client import connect
DISGUISE_IP = "director"
with connect(f"ws://{DISGUISE_IP}:80/api/session/liveupdate") as socket:
# send subscription messages and receive updates
pass Active records
Subscribe to object.getRecords to receive real-time updates for all currently playing clips. The update fires whenever the frame count changes on any active record, delivering a complete snapshot of all active records.
socket.onopen = () => {
socket.send(JSON.stringify({
subscribe: {
object: "subsystem:ProofOfPlaySubsystem",
properties: ["object.getRecords"]
}
}));
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (!data.valuesChanged) return;
for (const update of data.valuesChanged) {
for (const record of update.value) {
const pct = ((record.framesPlayed / record.totalFrames) * 100).toFixed(1);
console.log(`[${record.layerName}] ${record.videoName} - ${pct}% (${record.loopCount} loops)`);
}
}
}; import json
from websockets.sync.client import connect
with connect("ws://director:80/api/session/liveupdate") as socket:
socket.send(json.dumps({
"subscribe": {
"object": "subsystem:ProofOfPlaySubsystem",
"properties": ["object.getRecords"]
}
}))
while True:
message = json.loads(socket.recv())
if "valuesChanged" not in message:
continue
for update in message["valuesChanged"]:
for r in update["value"]:
pct = r["framesPlayed"] / r["totalFrames"] * 100
print(f"[{r['layerName']}] {r['videoName']} - {pct:.1f}% ({r['loopCount']} loops)") The active field is always true for records returned by this subscription.
Recent records
Subscribe to object.getRecentRecords(n) to receive updates each time a playback event completes and is archived. Replace n with the number of records to return (maximum 300); records are returned oldest-first.
socket.send(JSON.stringify({
subscribe: {
object: "subsystem:ProofOfPlaySubsystem",
properties: ["object.getRecentRecords(50)"]
}
})); socket.send(json.dumps({
"subscribe": {
"object": "subsystem:ProofOfPlaySubsystem",
"properties": ["object.getRecentRecords(50)"]
}
})) To change the number of records returned, unsubscribe and resubscribe with a different value:
socket.send(JSON.stringify({
unsubscribe: {
object: "subsystem:ProofOfPlaySubsystem",
properties: ["object.getRecentRecords(50)"]
}
}));
socket.send(JSON.stringify({
subscribe: {
object: "subsystem:ProofOfPlaySubsystem",
properties: ["object.getRecentRecords(100)"]
}
})); socket.send(json.dumps({
"unsubscribe": {
"object": "subsystem:ProofOfPlaySubsystem",
"properties": ["object.getRecentRecords(50)"]
}
}))
socket.send(json.dumps({
"subscribe": {
"object": "subsystem:ProofOfPlaySubsystem",
"properties": ["object.getRecentRecords(100)"]
}
})) ProofOfPlayRecord fields
Each record returned by either subscription contains the following fields:
| Field | Type | Description |
|---|---|---|
layerName | string | Name of the layer the clip played on |
videoName | string | Clip filename, with .apx extension and objects/videoclip/ prefix stripped |
framesPlayed | number | Total frames played, including repeats from looping |
totalFrames | number | Total frames in the clip |
loopCount | number | Complete loops played through the clip (framesPlayed / totalFrames) |
completedFullPlayback | bool | true if every unique frame in the clip was shown at least once |
startTime | string | ISO 8601 timestamp when playback started |
endTime | string | ISO 8601 timestamp when playback ended (or last active time, for active records) |
active | bool | true for currently playing records, false for completed records in history |
Complete example
This example subscribes to both active and recent records simultaneously and logs all updates:
const DISGUISE_IP = "director";
const RECENT_RECORD_COUNT = 50;
const socket = new WebSocket(`ws://${DISGUISE_IP}:80/api/session/liveupdate`);
socket.onopen = () => {
socket.send(JSON.stringify({
subscribe: {
object: "subsystem:ProofOfPlaySubsystem",
properties: [
"object.getRecords",
`object.getRecentRecords(${RECENT_RECORD_COUNT})`
]
}
}));
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
if (!data.valuesChanged) return;
for (const update of data.valuesChanged) {
const records = update.value;
if (update.path.includes("getRecentRecords")) {
console.log("Recently completed:");
for (const r of records) {
const status = r.completedFullPlayback ? "FULL" : "PARTIAL";
console.log(` [${r.layerName}] ${r.videoName} [${status}] ${r.startTime} - ${r.endTime}`);
}
} else {
console.log("Now playing:");
for (const r of records) {
const pct = ((r.framesPlayed / r.totalFrames) * 100).toFixed(1);
console.log(` [${r.layerName}] ${r.videoName} - ${pct}% (${r.loopCount} loops)`);
}
}
}
}; import json
from websockets.sync.client import connect
DISGUISE_IP = "director"
RECENT_RECORD_COUNT = 50
with connect(f"ws://{DISGUISE_IP}:80/api/session/liveupdate") as socket:
socket.send(json.dumps({
"subscribe": {
"object": "subsystem:ProofOfPlaySubsystem",
"properties": [
"object.getRecords",
f"object.getRecentRecords({RECENT_RECORD_COUNT})"
]
}
}))
while True:
message = json.loads(socket.recv())
if "valuesChanged" not in message:
continue
for update in message["valuesChanged"]:
records = update["value"]
if "getRecentRecords" in update["path"]:
print("Recently completed:")
for r in records:
status = "FULL" if r["completedFullPlayback"] else "PARTIAL"
print(f" [{r['layerName']}] {r['videoName']} [{status}] {r['startTime']} - {r['endTime']}")
else:
print("Now playing:")
for r in records:
pct = r["framesPlayed"] / r["totalFrames"] * 100
print(f" [{r['layerName']}] {r['videoName']} - {pct:.1f}% ({r['loopCount']} loops)") CSV export
Designer can also write proof of play records to a CSV file on disk. Enable this via the enableProofOfPlay debug option; records are written to output/proofofplay.csv in the d3 data directory and contain the same fields as the API.
The CSV is suited to generating offline reports. The Live Update API is recommended when integrating with external systems in real time.