disguise developers

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:

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:

FieldTypeDescription
layerNamestringName of the layer the clip played on
videoNamestringClip filename, with .apx extension and objects/videoclip/ prefix stripped
framesPlayednumberTotal frames played, including repeats from looping
totalFramesnumberTotal frames in the clip
loopCountnumberComplete loops played through the clip (framesPlayed / totalFrames)
completedFullPlaybackbooltrue if every unique frame in the clip was shown at least once
startTimestringISO 8601 timestamp when playback started
endTimestringISO 8601 timestamp when playback ended (or last active time, for active records)
activebooltrue 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.