Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/cgwire/zou/llms.txt

Use this file to discover all available pages before exploring further.

Kitsu provides a real-time event stream through WebSocket connections, allowing clients to receive live updates about changes in projects, tasks, comments, and other entities.

WebSocket Connection

Connect to the event stream using Socket.IO at the /events namespace:
import io from 'socket.io-client';

const socket = io('https://your-kitsu-instance.com', {
  path: '/socket.io',
  transports: ['websocket'],
  auth: {
    token: 'your-jwt-token'
  }
});

// Connect to the events namespace
const eventsSocket = socket.of('/events');

eventsSocket.on('connect', () => {
  console.log('Connected to event stream');
});

eventsSocket.on('disconnect', () => {
  console.log('Disconnected from event stream');
});

Authentication

The WebSocket connection requires JWT authentication. Include your JWT token in the connection parameters:
const socket = io('https://your-kitsu-instance.com', {
  auth: {
    token: localStorage.getItem('jwt_token')
  }
});

Preview Room Events

Preview rooms enable real-time collaborative review sessions where multiple users can synchronize their playback and annotations.

Join a Preview Room

socket.emit('preview-room:join', {
  playlist_id: 'a24a6ea4-ce75-4665-a070-57453082c25',
  user_id: 'b35b7fb5-df86-5776-b181-68564193d36',
  local_id: 'unique-client-id',
  is_playing: false,
  current_entity_id: null,
  current_entity_index: 0,
  current_preview_file_id: null,
  current_preview_file_index: 0,
  current_frame: 0,
  is_repeating: false,
  is_annotations_displayed: true,
  is_zoom_enabled: false,
  is_waveform_displayed: false,
  is_laser_mode: false,
  handle_in: null,
  handle_out: null,
  speed: 1.0,
  comparing: {
    enable: false,
    task_type: null,
    revision: null,
    mode: 'sidebyside',
    comparison_preview_index: 0
  }
});

Parameters

playlist_id
string
required
The UUID of the playlist to join
user_id
string
required
The UUID of the user joining the room
local_id
string
Unique client identifier for this connection
is_playing
boolean
default:false
Current playback state
current_entity_id
string
UUID of the currently viewed entity (shot/asset)
current_entity_index
number
Index of the current entity in the playlist
current_preview_file_id
string
UUID of the currently displayed preview file
current_preview_file_index
number
Index of the current preview file
current_frame
number
default:0
Current frame number in the preview
is_repeating
boolean
default:false
Whether playback should loop
is_annotations_displayed
boolean
default:false
Whether annotations should be visible
is_zoom_enabled
boolean
default:false
Whether zoom is enabled
is_waveform_displayed
boolean
default:false
Whether audio waveform should be displayed
is_laser_mode
boolean
default:false
Whether laser pointer mode is active
handle_in
number
In point for the preview range
handle_out
number
Out point for the preview range
speed
number
Playback speed multiplier
comparing
object
Comparison mode configuration
comparing.enable
boolean
default:false
Whether comparison mode is enabled
comparing.task_type
string
Task type UUID for comparison
comparing.revision
number
Revision number to compare
comparing.mode
string
default:"sidebyside"
Comparison display mode (sidebyside, overlay, etc.)
comparing.comparison_preview_index
number
default:0
Index of the preview being compared

Leave a Preview Room

socket.emit('preview-room:leave', {
  playlist_id: 'a24a6ea4-ce75-4665-a070-57453082c25'
});

Open Playlist (Observer Mode)

Join a playlist room as an observer without actively syncing:
socket.emit('preview-room:open-playlist', {
  playlist_id: 'a24a6ea4-ce75-4665-a070-57453082c25'
});

Close Playlist

socket.emit('preview-room:close-playlist', {
  playlist_id: 'a24a6ea4-ce75-4665-a070-57453082c25'
});

Update Room State

Broadcast state changes to all participants:
socket.emit('preview-room:room-updated', {
  playlist_id: 'a24a6ea4-ce75-4665-a070-57453082c25',
  user_id: 'b35b7fb5-df86-5776-b181-68564193d36',
  is_playing: true,
  current_frame: 150,
  current_entity_id: 'c46c8gc6-eg97-6887-c292-79675204e47',
  current_preview_file_id: 'd57d9hd7-fh08-7998-d403-80786315f58'
});

Receiving Preview Room Events

Listen for events from other participants:

Room Updated

socket.on('preview-room:room-updated', (data) => {
  console.log('Room state updated:', data);
  // Sync local player state with received data
  if (data.only_newcomer && !isNewcomer) {
    return; // Ignore updates meant only for newcomers
  }
  
  updatePlayerState({
    isPlaying: data.is_playing,
    currentFrame: data.current_frame,
    currentEntityId: data.current_entity_id,
    isAnnotationsDisplayed: data.is_annotations_displayed,
    speed: data.speed
  });
});

Response Fields

playlist_id
string
UUID of the playlist
user_id
string
UUID of the user who triggered the update
people
array
List of user IDs currently in the room
is_playing
boolean
Current playback state
current_entity_id
string
UUID of the currently viewed entity
current_frame
number
Current frame number
only_newcomer
boolean
If true, this update is only for users just joining

People Updated

socket.on('preview-room:room-people-updated', (data) => {
  console.log('People in room:', data.people);
  // Update UI to show who's in the room
  updateParticipantsList(data.people);
});

Response Fields

playlist_id
string
UUID of the playlist
people
array
Array of user IDs currently in the preview room

Annotation Events

Add Annotation

socket.emit('preview-room:add-annotation', {
  playlist_id: 'a24a6ea4-ce75-4665-a070-57453082c25',
  annotation: {
    x: 0.5,
    y: 0.3,
    text: 'Needs color correction',
    time: 150,
    drawing: null
  }
});

socket.on('preview-room:add-annotation', (data) => {
  // Another user added an annotation
  addAnnotationToCanvas(data.annotation);
});

Update Annotation

socket.emit('preview-room:update-annotation', {
  playlist_id: 'a24a6ea4-ce75-4665-a070-57453082c25',
  annotation_id: 'e68e0ie8-gi19-8009-e514-91897426g69',
  annotation: {
    x: 0.5,
    y: 0.35,
    text: 'Needs significant color correction'
  }
});

socket.on('preview-room:update-annotation', (data) => {
  updateAnnotation(data.annotation_id, data.annotation);
});

Remove Annotation

socket.emit('preview-room:remove-annotation', {
  playlist_id: 'a24a6ea4-ce75-4665-a070-57453082c25',
  annotation_id: 'e68e0ie8-gi19-8009-e514-91897426g69'
});

socket.on('preview-room:remove-annotation', (data) => {
  removeAnnotation(data.annotation_id);
});

Comparison Events

Change Version

socket.emit('preview-room:change-version', {
  playlist_id: 'a24a6ea4-ce75-4665-a070-57453082c25',
  revision: 2
});

socket.on('preview-room:change-version', (data) => {
  loadRevision(data.revision);
});

Pan & Zoom Changes

// Main viewport pan/zoom
socket.emit('preview-room:panzoom-changed', {
  playlist_id: 'a24a6ea4-ce75-4665-a070-57453082c25',
  x: 100,
  y: 50,
  scale: 1.5
});

socket.on('preview-room:panzoom-changed', (data) => {
  syncPanZoom(data.x, data.y, data.scale);
});

// Comparison viewport pan/zoom
socket.emit('preview-room:comparison-panzoom-changed', {
  playlist_id: 'a24a6ea4-ce75-4665-a070-57453082c25',
  x: 100,
  y: 50,
  scale: 1.5
});

socket.on('preview-room:comparison-panzoom-changed', (data) => {
  syncComparisonPanZoom(data.x, data.y, data.scale);
});

Connection Management

Connection Events

socket.on('connect', () => {
  console.log('Connected to event stream');
});

socket.on('disconnect', (reason) => {
  console.log('Disconnected:', reason);
  // Handle reconnection logic
});

socket.on('connect_error', (error) => {
  console.error('Connection error:', error);
});

Server Statistics

Get current server connection statistics:
cURL
curl https://your-kitsu-instance.com/stats
Response
{
  "nb_connections": 42
}

Complete Example

Here’s a complete example of setting up a preview room client:
import io from 'socket.io-client';

class PreviewRoomClient {
  constructor(kitsuUrl, jwtToken, playlistId) {
    this.playlistId = playlistId;
    this.socket = io(kitsuUrl, {
      path: '/socket.io',
      transports: ['websocket'],
      auth: { token: jwtToken }
    });
    
    this.setupEventListeners();
  }
  
  setupEventListeners() {
    const socket = this.socket;
    
    socket.on('connect', () => {
      console.log('Connected to Kitsu event stream');
      this.joinRoom();
    });
    
    socket.on('preview-room:room-updated', (data) => {
      this.handleRoomUpdate(data);
    });
    
    socket.on('preview-room:room-people-updated', (data) => {
      this.handlePeopleUpdate(data);
    });
    
    socket.on('preview-room:add-annotation', (data) => {
      this.handleAddAnnotation(data);
    });
    
    socket.on('preview-room:update-annotation', (data) => {
      this.handleUpdateAnnotation(data);
    });
    
    socket.on('preview-room:remove-annotation', (data) => {
      this.handleRemoveAnnotation(data);
    });
    
    socket.on('disconnect', () => {
      console.log('Disconnected from event stream');
    });
  }
  
  joinRoom() {
    this.socket.emit('preview-room:join', {
      playlist_id: this.playlistId,
      user_id: this.getCurrentUserId(),
      local_id: this.generateLocalId(),
      is_playing: false,
      current_frame: 0,
      is_annotations_displayed: true
    });
  }
  
  leaveRoom() {
    this.socket.emit('preview-room:leave', {
      playlist_id: this.playlistId
    });
  }
  
  updatePlaybackState(isPlaying, currentFrame) {
    this.socket.emit('preview-room:room-updated', {
      playlist_id: this.playlistId,
      user_id: this.getCurrentUserId(),
      is_playing: isPlaying,
      current_frame: currentFrame
    });
  }
  
  addAnnotation(annotation) {
    this.socket.emit('preview-room:add-annotation', {
      playlist_id: this.playlistId,
      annotation: annotation
    });
  }
  
  handleRoomUpdate(data) {
    // Sync local state with room state
    console.log('Room updated:', data);
  }
  
  handlePeopleUpdate(data) {
    console.log('People in room:', data.people);
  }
  
  handleAddAnnotation(data) {
    console.log('Annotation added:', data);
  }
  
  handleUpdateAnnotation(data) {
    console.log('Annotation updated:', data);
  }
  
  handleRemoveAnnotation(data) {
    console.log('Annotation removed:', data);
  }
  
  getCurrentUserId() {
    // Return current user's UUID
    return localStorage.getItem('user_id');
  }
  
  generateLocalId() {
    return `client-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
  }
  
  disconnect() {
    this.leaveRoom();
    this.socket.disconnect();
  }
}

// Usage
const client = new PreviewRoomClient(
  'https://your-kitsu-instance.com',
  'your-jwt-token',
  'playlist-uuid'
);

// When starting playback
client.updatePlaybackState(true, 0);

// When adding an annotation
client.addAnnotation({
  x: 0.5,
  y: 0.3,
  text: 'Fix this',
  time: 150
});

// Cleanup
client.disconnect();

Event Types

The Kitsu API emits various event types through the event stream for different entity changes:
  • notification:new - New notification created
  • notification:all-read - All notifications marked as read
  • task:update - Task updated
  • comment:new - New comment added
  • person:new - New person created
  • person:update - Person updated
  • person:delete - Person deleted
  • project:update - Project updated
  • project:delete - Project deleted
  • asset:new - New asset created
  • shot:new - New shot created
  • scene:new - New scene created
  • organisation:update - Organisation settings updated
These events are primarily used internally by the Kitsu web application for real-time UI updates.

Build docs developers (and LLMs) love