7.2 Build the playback token

key     = base64url_decode(playback_key)                          // 16 bytes
payload = {"exp": server_now + 300, "sid": session_id, "str": "<type>/<id>/<ext>"}
iv      = 12 random bytes                                         // new for every token
ct, tag = AES-128-GCM-encrypt(key, iv, utf8(json(payload)))       // no AAD, 16-byte tag
p       = base64url(iv || tag || ct)                              // note: tag BEFORE ciphertext
token   = p + "." + base64url(HMAC-SHA256(key, ascii(p)))
  • base64url without padding (=).
  • str: channel/<channel id>/ts or channel/<channel id>/m3u8; vod/<movie id>/<container_extension>; saga/<episode id>/<container_extension>.
  • WebCrypto's encrypt returns ct || tag: split the last 16 bytes off and reorder to iv || tag || ct.
  • Content outside the line's package is refused by the node (403).

Test vectors. With these inputs your code must produce exactly these tokens:

Vector 1 Vector 2
playback_key AAECAwQFBgcICQoLDA0ODw AAECAwQFBgcICQoLDA0ODw
key (hex) 000102030405060708090a0b0c0d0e0f same
iv (hex) a0a1a2a3a4a5a6a7a8a9aaab b0b1b2b3b4b5b6b7b8b9babb
payload {"exp":1767225600,"sid":"wSN7wDGh2fH4o6Ai1LxUAg","str":"channel/123/ts"} {"exp":1767225600,"sid":"wSN7wDGh2fH4o6Ai1LxUAg","str":"vod/4321/mp4"}
tag (hex) df94b875544f120892a13ab9d051d75f 705e61aec7ca9d1ef50ac1c51db91198

Vector 1 token:

oKGio6Slpqeoqaqr35S4dVRPEgiSoTq50FHXX9GkXcMOqwk7vU6CMnQnhlBjwgF76nMflQdnItV1-svv8pEjeTuF2iwW7668-ppu8pB2Pz1Bo6zP4V7vpIsAaFLAA4D9HwWzvw.EbSRReGnF1PpE7iG426GxgG9jmEncKdcOj8bdAn0Wzk

Vector 2 token:

sLGys7S1tre4ubq7cF5hrsfKnR71CsHFHbkRmCXr1eW3tREOiRxsNOdPKdbpf3UEoIY13IOBVPeNZGaA880rY5YNtaMbP-JgJZVG5jyalBK4xDrNzXGF6i5D1oESAXdi_9Q.e_vR9ATz3Xp3Vdey0QZcCf19BNcExPx8dZf1j-noOEA

The JSON is serialized exactly as shown (no spaces, keys in that order). Any valid JSON works for the node; the fixed form only makes the vectors reproducible.

7.3 Clock

The node refuses a token whose exp has passed or is more than 600 s in the future. TV clocks are often wrong, so never use the device clock directly:

offset     = server_time - device_now      // from login / refresh-session (or identity.timestamp)
server_now = device_now + offset

Update offset on every login and refresh-session.