← Cove Labs

Overview

HZMediaP is a control-plane-only protocol. It handles authentication, search, session negotiation, and playlist management. Media bytes (HLS segments) never flow through the WebSocket — they are fetched directly from the CDN by the client after receiving a signed URL via STREAM_READY.

Connect to the server at wss://<host>/ws. The server speaks HZMediaP 1.0 over a standard WebSocket connection using text frames (UTF-8 JSON). All messages use the same envelope structure regardless of direction.

Control plane only. HZMediaP manages sessions and delivers signed CDN URLs. Your player fetches HLS directly from the CDN — do not attempt to pipe media through the WebSocket.

Message types at a glance

TypeDirDescription
HANDSHAKEC → SOpen connection, identify platform and capabilities
HANDSHAKE_ACKS → CSession nonce, heartbeat config, server version
AUTHC → SAuthenticate with Google ID token + nonce
AUTH_OKS → CJWT access token + user profile
AUTH_REFRESHS → CServer-pushed updated access token
SEARCHC → SSearch or browse the content library
SEARCH_RESULTS → CPaginated result set with relevance scores
STREAM_INITC → SRequest to begin streaming a content item
CONTENT_METAS → CFull metadata for the requested content item
STREAM_READYS → CSigned HLS URL — start playback
PROGRESS_REPORTC → SPlayback position, quality, buffer telemetry
PLAYLIST_REQUESTC → SCRUD operations on the user's playlists
PLAYLIST_RESPONSES → CPlaylist data in response to a request
HEARTBEATC → SKeepalive ping (must be sent every 30 s)
HEARTBEAT_ACKS → CKeepalive acknowledgement with server timestamp
ERRORS → CError with code, domain, message, and retryable flag

Envelope format

Every message — in both directions — uses the same JSON envelope. The payload field varies by type.

EnvelopeJSON
{
  "hzmp":    "1.0",        // protocol version — must be "1.0"
  "msgid":   "<uuid-v4>",  // unique message ID
  "type":    "STREAM_INIT",// message type constant
  "ts":      1712000000000,// sender Unix timestamp (ms)
  "ref":     null,          // msgid of the message being replied to, or null
  "payload": { /* type-specific */ }
}
FieldTypeDescription
hzmpstringrequiredProtocol version. Must be "1.0". The server checks the major version and rejects mismatches with error 1004.
msgidstringrequiredUUID v4 uniquely identifying this message. Used for correlating responses via ref.
typestringrequiredOne of the message type constants listed in the overview.
tsnumberrequiredUnix timestamp in milliseconds at time of sending.
refstring|nullrequiredWhen the server sends a reply, ref is the msgid of the request. Set to null for unsolicited messages.
payloadobjectrequiredMessage-specific payload object. Never null — use {} for messages with no fields.
Max message size is 256 KB. Messages exceeding this limit are rejected with error 1003 and the connection is closed.

Connection flow

Every session follows this sequence. No other messages are accepted before AUTH_OK.

client → HANDSHAKE identify client name, platform, version, capabilities
server → HANDSHAKE_ACK session_nonce, heartbeat_interval_ms, max_message_size
client → AUTH Google ID token + nonce from HANDSHAKE_ACK
server → AUTH_OK access_token (JWT), user profile, expires_in
client → STREAM_INIT / SEARCH / PLAYLIST_REQUEST session is ready
client → HEARTBEAT every heartbeat_interval_ms (default 30 s)
server → HEARTBEAT_ACK missed 2× intervals → server closes connection
Max 5 concurrent sessions per user. Exceeding this limit returns error 4005. Previous sessions remain open — the client must explicitly disconnect old ones or let them expire via missed heartbeats.

Quick start

A minimal JavaScript client that authenticates, searches for content, then initiates streaming. Swap getGoogleIdToken() with your auth provider's SDK call.

Minimal clientJavaScript
const ws = new WebSocket('wss://stream.covelabs.io/ws');
let sessionNonce, accessToken;

function send(type, payload, ref = null) {
  ws.send(JSON.stringify({
    hzmp:  '1.0',
    msgid: crypto.randomUUID(),
    type, ts: Date.now(), ref, payload,
  }));
}

ws.onopen = () => {
  send('HANDSHAKE', {
    client_name:    'my-app',
    client_version: '1.0.0',
    platform:       'web',
    capabilities:   ['hls'],
  });
};

ws.onmessage = async ({ data }) => {
  const msg = JSON.parse(data);

  switch (msg.type) {
    case 'HANDSHAKE_ACK': {
      sessionNonce = msg.payload.session_nonce;
      const idToken = await getGoogleIdToken();
      send('AUTH', {
        strategy:  'firebase',
        id_token:  idToken,
        nonce:     sessionNonce,
      });
      break;
    }
    case 'AUTH_OK': {
      accessToken = msg.payload.access_token;
      // Session ready — send a search
      send('SEARCH', { query: 'late night jazz', mode: 'hybrid' });
      // Start heartbeat loop
      setInterval(() => send('HEARTBEAT', {}),
        msg.payload.heartbeat_interval_ms);
      break;
    }
    case 'SEARCH_RESULT': {
      const first = msg.payload.items[0];
      if (first) send('STREAM_INIT', {
        content_id: first.content_id,
        resume:     true,
      });
      break;
    }
    case 'STREAM_READY': {
      // Pass hls_master_url to your player (e.g. hls.js)
      player.loadSource(msg.payload.hls_master_url);
      break;
    }
    case 'ERROR': {
      console.error(msg.payload.code, msg.payload.message);
      if (msg.payload.retryable) /* retry with backoff */;
      break;
    }
  }
};

Authentication

HANDSHAKE

The first message sent by the client on every new connection.

FieldTypeDescription
client_namestringrequiredHuman-readable client identifier, e.g. "my-ios-app"
client_versionstringrequiredSemver string of your client build
platformstringrequiredOne of "ios", "android", "web", "desktop"
os_versionstringoptionalOS version string, e.g. "17.4"
capabilitiesstring[]requiredSupported features. Include "hls" for HLS playback.
localestringoptionalBCP 47 locale, e.g. "en-US"
timezonestringoptionalIANA timezone, e.g. "America/New_York"

HANDSHAKE_ACK

FieldTypeDescription
server_versionstringServer software version
session_noncestringBase64-encoded 32-byte random nonce — include in AUTH payload
auth_requiredbooleanAlways true in the current server
heartbeat_interval_msnumberHow often to send HEARTBEAT (currently 30000)
max_message_sizenumberMaximum message size in bytes (currently 262144)

AUTH

FieldTypeDescription
strategystringrequiredMust be "firebase"
id_tokenstringrequiredGoogle ID token from Firebase Auth SDK
noncestringrequiredThe session_nonce from HANDSHAKE_ACK

AUTH_OK

FieldTypeDescription
user_idstringUnique user identifier
display_namestringUser's display name from Google profile
avatar_urlstring|nullProfile photo URL
access_tokenstringJWT — include as Bearer token in any REST requests
expires_innumberToken validity in seconds
refresh_beforenumberUnix ms — server pushes AUTH_REFRESH before this time

Streaming

Send STREAM_INIT to begin a stream. The server responds with CONTENT_META then STREAM_READY. Feed hls_master_url to your HLS player — no media bytes come through the WebSocket.

STREAM_INIT payload

FieldTypeDescription
content_idstringrequiredID from a SEARCH_RESULT item
preferred_qualitystringoptionale.g. "1080p", "720p", "360p", "320k". Default: "auto"
start_position_snumberoptionalStart offset in seconds. Default 0
resumebooleanoptionalIf true, server uses last saved position for this user + content pair
playlist_idstring|nulloptionalAssociate this stream session with a playlist context

STREAM_READY payload

FieldTypeDescription
content_idstring
session_idstringUse in PROGRESS_REPORT
hls_master_urlstringSigned HLS master playlist URL. Load this into your player.
start_position_snumberWhere to start playback (respects resume)
token_expires_atnumberUnix ms — URL expires at this time. Listen for error 6004 and re-init.
CDN URLs expire after 60 minutes. The server caches signed URLs for 55 minutes. If playback is still active near expiry, you will receive error 6004 TOKEN_REFRESH_NEEDED — send a new STREAM_INIT with the same content_id to get a fresh URL.

Progress reporting

Send PROGRESS_REPORT periodically during playback. The server records position for resume support and updates play history. No response is sent. Recommended interval: every 10–15 seconds.

FieldTypeDescription
session_idstringrequiredFrom STREAM_READY
content_idstringrequired
position_snumberrequiredCurrent playback position in seconds
duration_snumberrequiredTotal content duration in seconds
percentnumberrequired0–100
buffered_ahead_snumberoptionalSeconds of buffer ahead of playhead
qualitystringoptionalActive HLS rendition, e.g. "720p"
bandwidth_bpsnumberoptionalEstimated available bandwidth
stall_countnumberoptionalNumber of buffer stalls since last report
stall_duration_msnumberoptionalTotal stall time since last report in ms

Playlists

All playlist operations go through a single PLAYLIST_REQUEST message, differentiated by the action field. Max 50 playlists per user.

ActionRequired fieldsDescription
listReturn all playlists owned by the authenticated user
getplaylist_idReturn a playlist's full item list, ordered by position
createnameCreate a new playlist. Optional: description, is_public
add_itemplaylist_id, content_idAppend an item. Returns error 7002 if already present
remove_itemplaylist_id, content_idRemove an item. Returns error 7003 if not found
reorderplaylist_id, content_id, positionMove an item to the given position (1-indexed)
deleteplaylist_idPermanently delete a playlist and all its items
Example — create then add an itemJSON
// Create
{ "type": "PLAYLIST_REQUEST", "payload": {
  "action": "create", "name": "Evening Wind-Down"
} }

// Server responds with new playlist_id in PLAYLIST_RESPONSE

// Add item
{ "type": "PLAYLIST_REQUEST", "payload": {
  "action":      "add_item",
  "playlist_id": "pl_abc123",
  "content_id":  "ct_xyz789"
} }

Heartbeat

The server closes the connection if no HEARTBEAT is received within two heartbeat intervals (default: 60 s). Send HEARTBEAT at the heartbeat_interval_ms cadence provided in HANDSHAKE_ACK.

HEARTBEATJSON
{ "type": "HEARTBEAT", "payload": { "session_id": null } }

// Server responds:
{ "type": "HEARTBEAT_ACK", "payload": { "server_ts": 1712000030000 } }

Error codes

The server sends an ERROR message whenever an operation fails. Check payload.retryable — if true, retry with exponential backoff. If false, the error requires client action to resolve.

Protocol (1xxx)
CodeNameMessageRetryable
1000UNKNOWNUnknown erroryes
1001INVALID_ENVELOPEMalformed JSON or missing required envelope fieldsno
1002UNKNOWN_MESSAGE_TYPEUnrecognised type fieldno
1003MESSAGE_TOO_LARGEMessage exceeds 256 KBno
1004VERSION_MISMATCHMajor version not supportedno
Auth (4xxx)
CodeNameMessageRetryable
4000AUTH_REQUIREDAttempted operation before AUTH_OKno
4001TOKEN_EXPIREDJWT expired — re-authenticateyes
4002TOKEN_INVALIDJWT signature invalidno
4003OAUTH_REJECTEDGoogle ID token rejectedno
4004ACCOUNT_SUSPENDEDAccount suspendedno
4005SESSION_LIMIT_EXCEEDEDMax 5 concurrent sessions per useryes
Search (5xxx)
CodeNameMessageRetryable
5000SEARCH_FAILEDInternal search erroryes
5001QUERY_TOO_LONGQuery exceeds 512 charactersno
5002INVALID_FILTERUnrecognised filter field or valueno
Stream (6xxx)
CodeNameMessageRetryable
6000CONTENT_NOT_FOUNDNo content with the given IDno
6001NOT_AUTHORIZEDUser does not have access to this contentno
6002CONTENT_UNAVAILABLEContent exists but cannot be streamedyes
6003SESSION_NOT_FOUNDStream session ID not in Redisyes
6004TOKEN_REFRESH_NEEDEDCDN signed URL expiring — send new STREAM_INITyes
6005TRANSCODING_IN_PROGRESSContent not yet ready — poll or waityes
Playlist (7xxx)
CodeNameMessageRetryable
7000PLAYLIST_NOT_FOUNDNo playlist with the given IDno
7001PLAYLIST_LIMIT_EXCEEDEDUser has reached the 50-playlist limitno
7002ITEM_ALREADY_IN_PLAYLISTDuplicate item — already in this playlistno
7003ITEM_NOT_IN_PLAYLISTItem not found in this playlistno
Server (9xxx)
CodeNameMessageRetryable
9000INTERNAL_ERRORUnexpected server erroryes
9001RATE_LIMITEDToo many messages — slow down. Check retry_after_msyes
9002SERVICE_UNAVAILABLEServer temporarily unavailableyes

Limits

LimitValueError on breach
Max message size256 KB1003
Max query length512 chars5001
Max search results per page100Clamped silently
Max concurrent sessions per user54005
Max playlists per user507001
Rate limit50 msg/s9001 with retry_after_ms
Heartbeat timeout2 × interval (60 s)Connection closed
CDN URL TTL60 min6004 at 55 min