logo
Hello World!

BetaHub Video Events Sync

July 3, 2025
typescriptbetahubpackage

Advanced package for precise synchronization of time-based events (logs, interactions, errors) with video playback.

The project was created in mid-2025 as a response to specific needs of the BetaHub platform in the area of video game testing. It's an advanced npm package that enables precise synchronization of time-based events (logs, interactions, errors) with video playback.

The problem that the library solves is characteristic of the gamedev industry: during game testing, logs are generated in real-time while gameplay is being recorded. Later, during video analysis, it's crucial to display relevant logs at the exact moments when they occurred during the game.

This is a computational library, consciously designed without user interface elements. This approach offers complete freedom in frontend implementation - developers can integrate it with any UI framework. The library provides only time synchronization algorithms and a callback system, leaving visualization in the hands of the programmer.

Below is an example interface implementation:



The library handles asynchronous loading and processing of files in JSONL (JSON Lines) format, where each line represents a single event. The loading process is fully asynchronous with a callback system for monitoring progress:

typescript
const bhves = new BHVESInstance({
  videoPlayerDomId: 'video-player',
  startTimestamp: '2025-06-12T14:03:20',
  onStateUpdate: ({ state, data }) => {
    // Update interface when state changes
  }
});

await bhves.addData({
  entries: [
    { 
      name: 'logs', 
      dataJSONL: '{"start_timestamp":"2025-06-12T14:03:25","type":"error","message":"Collision detection error"}\n{"start_timestamp":"2025-06-12T14:03:30","type":"info","message":"Player collected item"}'
    }
  ],
  options: {
    sortData: true,
    onProgress: (status) => {
      // Monitor processing progress
    },
    onSuccess: (loadedData) => {
      // Data successfully loaded and indexed
    },
    onError: (error) => {
      // Handle processing errors
    }
  }
});
const bhves = new BHVESInstance({
  videoPlayerDomId: 'video-player',
  startTimestamp: '2025-06-12T14:03:20',
  onStateUpdate: ({ state, data }) => {
    // Update interface when state changes
  }
});

await bhves.addData({
  entries: [
    { 
      name: 'logs', 
      dataJSONL: '{"start_timestamp":"2025-06-12T14:03:25","type":"error","message":"Collision detection error"}\n{"start_timestamp":"2025-06-12T14:03:30","type":"info","message":"Player collected item"}'
    }
  ],
  options: {
    sortData: true,
    onProgress: (status) => {
      // Monitor processing progress
    },
    onSuccess: (loadedData) => {
      // Data successfully loaded and indexed
    },
    onError: (error) => {
      // Handle processing errors
    }
  }
});

  • onProgress: Called during processing with progress information (0-100%)
  • onSuccess: Called when all data has been successfully loaded and indexed
  • onError: Called in case of parsing, validation, or indexing errors

The library uses two main callbacks:

  • onTimeUpdate: Called on every video time change
  • onStateUpdate: Called when the set of active events changes

Automatic connection to any HTML5 <video> element:

typescript
// Automatic connection by ID
const bhves = new BHVESInstance({
  videoPlayerDomId: 'my-video-player',
  // ...
});
// Automatic connection by ID
const bhves = new BHVESInstance({
  videoPlayerDomId: 'my-video-player',
  // ...
});

Ability to create and manage multiple independent instances for different players on the same page.

The choice of TypeScript was strategic for several reasons:

  • Type Safety: Complex API with multiple configuration options requires a strong type system
  • Developer Experience: IntelliSense and validation while writing code for teams using the library
  • Maintainability: Facilitated refactoring and development in the team

The library uses microbundle for building, which provides:

  • Small build size
  • TypeScript definitions: Automatic generation of .d.ts files

The conscious decision to create a computational library without UI means:

  • Freedom in UI implementation: Works with React, Vue, Angular or vanilla JavaScript
  • Separation of concerns: Synchronization algorithms separated from presentation layer
  • Easier testing: Core logic can be tested independently of the interface

The system was designed following the "separation of concerns" pattern, where each component has a clearly defined responsibility.

The main class that manages the entire system:

  • Initialization: Connects to video element and configures callbacks
  • Event handling: Listens to timeupdate events from the player
  • Time synchronization: Converts video time to timestamps
  • Communication: Calls user callbacks with current data

Central repository for all data. Stores data in two formats.

typescript
private data: { [key: string]: Data[] } = {};           // Original format
private dataInternal: { [key: string]: DataInternal[] } = {}; // Optimized format for searching
private data: { [key: string]: Data[] } = {};           // Original format
private dataInternal: { [key: string]: DataInternal[] } = {}; // Optimized format for searching
  • Dual data format: Original data for user, parsed for performance
  • One-time conversion: Timestamps parsed once during loading

Indexing system based on binary search:

  • Sorted indexes: Automatic sorting of events by timestamps
  • Binary search: O(log n) complexity for searching in large datasets

Class Diagram

The diagram shows relationships between main components:

  • BHVESInstance as API entry point
  • DataStore as central data repository
  • DataIndexManager as search engine
  • Utility functions as helper functions for the user

Sequence Diagram

The diagram shows data flow during typical lifecycle:

  1. Initialization: Instance creation and video connection
  2. Data loading: JSONL processing and index building
  3. Runtime: Continuous synchronization during playback
  4. Callbacks: State change notifications

typescript
const videoPlayerTimeMilliseconds = videoPlayerTimeSeconds * 1000;
const absoluteTimestampMs = this.startTimestampParsed.getTime() + videoPlayerTimeMilliseconds;
const currentTimeDate = new Date(absoluteTimestampMs);

// Finding matching events
const matchingIndexes = this.dataStore.findMatchingIndexes(currentTimeDate);
const videoPlayerTimeMilliseconds = videoPlayerTimeSeconds * 1000;
const absoluteTimestampMs = this.startTimestampParsed.getTime() + videoPlayerTimeMilliseconds;
const currentTimeDate = new Date(absoluteTimestampMs);

// Finding matching events
const matchingIndexes = this.dataStore.findMatchingIndexes(currentTimeDate);

The system operates in real-time, converting relative video player time to absolute timestamp, then finding all events that should be active at that moment.

typescript
import { BHVESInstance } from 'betahub-video-events-sync';

const bhves = new BHVESInstance({
  videoPlayerDomId: 'main-video-player',
  startTimestamp: '2025-06-12T14:03:20', // Recording start moment
  onTimeUpdate: ({ videoPlayerTimeSeconds, timestamp }) => {
    // Update system time of logs in interface
    document.getElementById('system-time').textContent = timestamp;
  },
  onStateUpdate: ({ state, data }) => {
    // React to changes in active events
    updateEventDisplay(state, data);
  }
});
import { BHVESInstance } from 'betahub-video-events-sync';

const bhves = new BHVESInstance({
  videoPlayerDomId: 'main-video-player',
  startTimestamp: '2025-06-12T14:03:20', // Recording start moment
  onTimeUpdate: ({ videoPlayerTimeSeconds, timestamp }) => {
    // Update system time of logs in interface
    document.getElementById('system-time').textContent = timestamp;
  },
  onStateUpdate: ({ state, data }) => {
    // React to changes in active events
    updateEventDisplay(state, data);
  }
});

typescript
await bhves.addData({
  entries: [
    {
      name: 'game_logs',
      dataJSONL: `{"start_timestamp":"2025-06-12T14:03:25","type":"error","message":"NullReferenceException in PlayerController"}
{"start_timestamp":"2025-06-12T14:03:30","type":"info","message":"Player entered new area"}
{"start_timestamp":"2025-06-12T14:03:35","type":"warning","message":"FPS dropped below 30"}`
    },
    {
      name: 'user_interactions', 
      dataJSONL: `{"start_timestamp":"2025-06-12T14:03:22","end_timestamp":"2025-06-12T14:03:24","type":"click","message":"Attack button click"}
{"start_timestamp":"2025-06-12T14:03:28","end_timestamp":"2025-06-12T14:03:32","type":"drag","message":"Dragging item to inventory"}`
    }
  ],
  options: {
    sortData: true,
    onProgress: (status) => {
      const progressBar = document.getElementById('progress');
      progressBar.style.width = `${status.progress}%`;
      
      if (status.status === 'loading') {
        console.log(`Loading: ${status.progress}%`);
      }
    },
    onSuccess: (loadedData) => {
      console.log('Loaded categories:', Object.keys(loadedData));
      showNotification('Data loaded successfully!');
    },
    onError: (error) => {
      console.error('Loading error:', error.message);
      showErrorDialog(error.message);
    }
  }
});
await bhves.addData({
  entries: [
    {
      name: 'game_logs',
      dataJSONL: `{"start_timestamp":"2025-06-12T14:03:25","type":"error","message":"NullReferenceException in PlayerController"}
{"start_timestamp":"2025-06-12T14:03:30","type":"info","message":"Player entered new area"}
{"start_timestamp":"2025-06-12T14:03:35","type":"warning","message":"FPS dropped below 30"}`
    },
    {
      name: 'user_interactions', 
      dataJSONL: `{"start_timestamp":"2025-06-12T14:03:22","end_timestamp":"2025-06-12T14:03:24","type":"click","message":"Attack button click"}
{"start_timestamp":"2025-06-12T14:03:28","end_timestamp":"2025-06-12T14:03:32","type":"drag","message":"Dragging item to inventory"}`
    }
  ],
  options: {
    sortData: true,
    onProgress: (status) => {
      const progressBar = document.getElementById('progress');
      progressBar.style.width = `${status.progress}%`;
      
      if (status.status === 'loading') {
        console.log(`Loading: ${status.progress}%`);
      }
    },
    onSuccess: (loadedData) => {
      console.log('Loaded categories:', Object.keys(loadedData));
      showNotification('Data loaded successfully!');
    },
    onError: (error) => {
      console.error('Loading error:', error.message);
      showErrorDialog(error.message);
    }
  }
});

typescript
import { 
  getMatchingData, 
  getMovingWindowIndexes, 
  getShiftedIndexes,
  getSecondsFromTimestamp 
} from 'betahub-video-events-sync';

function updateEventDisplay(state, data) {
  // 1. Get currently active events
  const activeEvents = getMatchingData(state.activeMatchingIndexes, data);
  
  // 2. Create "moving window" - show context
  const { prepend, append } = getMovingWindowIndexes(
    state.activeMatchingIndexes,
    data,
    {
      prependSize: 3,    // 3 previous
      appendSize: 5,     // 5 next  
      minimumSize: 10    // minimum 10 together
    }
  );
  
  const previousEvents = getMatchingData(prepend, data);
  const upcomingEvents = getMatchingData(append, data);
  
  // 3. Display in interface
  renderEventsList('active', activeEvents);
  renderEventsList('previous', previousEvents, { opacity: 0.5 });
  renderEventsList('upcoming', upcomingEvents, { opacity: 0.7 });
}

// Navigation to specific moment
function jumpToEvent(eventTimestamp) {
  const videoTime = getSecondsFromTimestamp(
    '2025-06-12T14:03:20', // recording start
    eventTimestamp         // event moment
  );
  
  document.getElementById('video-player').currentTime = videoTime;
}
import { 
  getMatchingData, 
  getMovingWindowIndexes, 
  getShiftedIndexes,
  getSecondsFromTimestamp 
} from 'betahub-video-events-sync';

function updateEventDisplay(state, data) {
  // 1. Get currently active events
  const activeEvents = getMatchingData(state.activeMatchingIndexes, data);
  
  // 2. Create "moving window" - show context
  const { prepend, append } = getMovingWindowIndexes(
    state.activeMatchingIndexes,
    data,
    {
      prependSize: 3,    // 3 previous
      appendSize: 5,     // 5 next  
      minimumSize: 10    // minimum 10 together
    }
  );
  
  const previousEvents = getMatchingData(prepend, data);
  const upcomingEvents = getMatchingData(append, data);
  
  // 3. Display in interface
  renderEventsList('active', activeEvents);
  renderEventsList('previous', previousEvents, { opacity: 0.5 });
  renderEventsList('upcoming', upcomingEvents, { opacity: 0.7 });
}

// Navigation to specific moment
function jumpToEvent(eventTimestamp) {
  const videoTime = getSecondsFromTimestamp(
    '2025-06-12T14:03:20', // recording start
    eventTimestamp         // event moment
  );
  
  document.getElementById('video-player').currentTime = videoTime;
}

typescript
// Handling two players - main and comparison
const mainRecording = new BHVESInstance({
  videoPlayerDomId: 'main-video',
  startTimestamp: '2025-06-12T14:03:20',
  onStateUpdate: ({ state, data }) => {
    updateMainTimeline(state, data);
  }
});

const comparisonRecording = new BHVESInstance({
  videoPlayerDomId: 'comparison-video', 
  startTimestamp: '2025-06-12T15:30:00', // Different recording
  onStateUpdate: ({ state, data }) => {
    updateComparisonTimeline(state, data);
  }
});

// Loading different data sets
await Promise.all([
  mainRecording.addData({
    entries: [{ name: 'session_1_logs', dataJSONL: session1Data }]
  }),
  comparisonRecording.addData({
    entries: [{ name: 'session_2_logs', dataJSONL: session2Data }]
  })
]);
// Handling two players - main and comparison
const mainRecording = new BHVESInstance({
  videoPlayerDomId: 'main-video',
  startTimestamp: '2025-06-12T14:03:20',
  onStateUpdate: ({ state, data }) => {
    updateMainTimeline(state, data);
  }
});

const comparisonRecording = new BHVESInstance({
  videoPlayerDomId: 'comparison-video', 
  startTimestamp: '2025-06-12T15:30:00', // Different recording
  onStateUpdate: ({ state, data }) => {
    updateComparisonTimeline(state, data);
  }
});

// Loading different data sets
await Promise.all([
  mainRecording.addData({
    entries: [{ name: 'session_1_logs', dataJSONL: session1Data }]
  }),
  comparisonRecording.addData({
    entries: [{ name: 'session_2_logs', dataJSONL: session2Data }]
  })
]);

The library uses JSONL (JSON Lines) format, where each line is a separate event in JSON format. This format was chosen due to:

  • Parsing efficiency: Each line can be processed independently
  • Compatibility: Standard format used in logs and data streaming
  • Readability: Easy to debug and manually edit

typescript
interface Data {
  start_timestamp: string;    // Event start timestamp
  end_timestamp?: string;     // Optional end timestamp
  type: string;               // Event type (log, interaction, error, any other)
  message?: string;           // Message content
  details?: object;           // Additional metadata
}
interface Data {
  start_timestamp: string;    // Event start timestamp
  end_timestamp?: string;     // Optional end timestamp
  type: string;               // Event type (log, interaction, error, any other)
  message?: string;           // Message content
  details?: object;           // Additional metadata
}

json
{"start_timestamp":"2025-06-12T14:03:20","type":"game_start","message":"Test session start","details":{"level":"tutorial","tester_id":"user_123"}}
{"start_timestamp":"2025-06-12T14:03:25","type":"error","message":"Texture loading failed","details":{"asset":"player_texture.png","error_code":"404"}}
{"start_timestamp":"2025-06-12T14:03:22","end_timestamp":"2025-06-12T14:03:28","type":"interaction","message":"Dialogue window","details":{"npc_name":"Village Elder","choices_shown":3}}
{"start_timestamp":"2025-06-12T14:03:20","type":"game_start","message":"Test session start","details":{"level":"tutorial","tester_id":"user_123"}}
{"start_timestamp":"2025-06-12T14:03:25","type":"error","message":"Texture loading failed","details":{"asset":"player_texture.png","error_code":"404"}}
{"start_timestamp":"2025-06-12T14:03:22","end_timestamp":"2025-06-12T14:03:28","type":"interaction","message":"Dialogue window","details":{"npc_name":"Village Elder","choices_shown":3}}

  • start_timestamp: Required - event start timestamp
  • end_timestamp: Optional - for events lasting over time
  • type: Required - event categorization
  • message: Optional - readable description
  • details: Optional - additional metadata in object format

Problem: Event synchronization with millisecond precision was critical for user experience. Differences of even 100-200ms would be noticeable during test video analysis. Often videos are analyzed frame by frame.

Solution:

  • Using ISO 8601 format for all timestamps
  • Parsing all timestamps during data loading

Problem: Test sessions can generate millions of events. Linear search in real-time would have high performance impact.

Solution:

  • Advanced indexing system in DataIndexManager
  • One-time processing of all timestamps during data loading
  • Binary search instead of linear for efficient data access

Processing Optimizations:

  • Chunked processing (10,000 records per chunk) with setTimeout(0) to avoid blocking UI
  • Parallel processing: ability to load multiple files simultaneously
  • Optional data sorting during loading
  • Dual storage: original data for API, optimized for searching

Problem: The library must be useful with React, Vue, Angular and vanilla JS, without imposing specific implementation style.

Solution:

  • Callback-based architecture offering both maximum flexibility and ease of use
  • State system containing current player time, current timestamp, and event indexes
typescript
interface State {
  videoPlayerTimeSeconds: number;
  timestamp: string;
  matchingIndexes: CategoryIndexes;     // All active events
  activeMatchingIndexes: CategoryIndexes; // Latest from each category
}
interface State {
  videoPlayerTimeSeconds: number;
  timestamp: string;
  matchingIndexes: CategoryIndexes;     // All active events
  activeMatchingIndexes: CategoryIndexes; // Latest from each category
}

Key Design Decisions:

  • Separation of matchingIndexes vs activeMatchingIndexes for different use cases
  • Helper functions instead of UI components
  • No external dependencies for maximum compatibility

BetaHub Video Events Sync is a specialized library that solves a real problem in the game development industry - precise synchronization of time-based events with gameplay recordings. Its creation was driven by specific needs of the BetaHub platform in the area of game test analysis.

This project was a fascinating experience in library design.

It confirmed my belief that the best projects arise from solving real, challenging business problems - not from abstract technical assumptions.

©2025 BatStack