Skip to main content

Timeline

The timeline provides a chronological view of all your captured screen and audio data, allowing you to browse, filter, and replay your digital history.

Timeline API

Fetch Timeline Data

curl "http://localhost:3030/timeline?limit=50&offset=0"

Query Parameters

limit
integer
default:"20"
Number of items to return (max 1000).
offset
integer
default:"0"
Pagination offset.
start_time
datetime
Start of time range (ISO 8601 or Unix timestamp).
end_time
datetime
End of time range (ISO 8601 or Unix timestamp).
content_type
enum
default:"all"
Filter by content type: all, ocr, audio, ui.
app_name
string
Filter by application name.
window_name
string
Filter by window title (partial match).
include_frames
boolean
default:"false"
Include base64-encoded frame images.

Timeline Features

Chronological View

The timeline returns all content (screen + audio) sorted by timestamp:
{
  "data": [
    {
      "type": "OCR",
      "content": {
        "timestamp": "2026-03-08T14:23:00Z",
        "text": "screenpipe documentation",
        "app_name": "Chrome",
        "window_name": "docs.screenpi.pe",
        "file_path": "~/.screenpipe/data/2026-03-08/1709911380000_m0.jpg",
        "text_source": "accessibility",
        "capture_trigger": "app_switch"
      }
    },
    {
      "type": "Audio",
      "content": {
        "timestamp": "2026-03-08T14:23:30Z",
        "transcription": "Let's review the API documentation.",
        "device_name": "MacBook Pro Microphone",
        "speaker_id": 0
      }
    },
    {
      "type": "OCR",
      "content": {
        "timestamp": "2026-03-08T14:24:15Z",
        "text": "GET /search endpoint parameters",
        "app_name": "Chrome",
        "window_name": "API Reference - screenpipe",
        "file_path": "~/.screenpipe/data/2026-03-08/1709911455000_m0.jpg",
        "text_source": "accessibility",
        "capture_trigger": "scroll_stop"
      }
    }
  ],
  "pagination": {
    "limit": 50,
    "offset": 0,
    "total": 1247
  }
}

Time-Based Navigation

# Last hour
curl "http://localhost:3030/timeline?start_time=$(date -u -v-1H +%Y-%m-%dT%H:%M:%SZ)&limit=100"

# Last 24 hours
curl "http://localhost:3030/timeline?start_time=$(date -u -v-24H +%Y-%m-%dT%H:%M:%SZ)&limit=500"
Every frame has a direct URL for viewing:
# Get specific frame
GET /frames/{frame_id}

# Example
curl "http://localhost:3030/frames/12345"
New snapshot frames are served directly as JPEG (less than 5ms). Old video-chunk frames are extracted via FFmpeg (100-500ms) for backward compatibility.

UI Implementation

Infinite Scroll Timeline

import { useState, useEffect, useRef } from 'react';

function Timeline() {
  const [items, setItems] = useState([]);
  const [offset, setOffset] = useState(0);
  const [hasMore, setHasMore] = useState(true);
  const observer = useRef();
  
  const loadMore = async () => {
    const response = await fetch(
      `http://localhost:3030/timeline?limit=50&offset=${offset}`
    );
    const data = await response.json();
    
    setItems(prev => [...prev, ...data.data]);
    setOffset(prev => prev + 50);
    setHasMore(data.pagination.total > offset + 50);
  };
  
  useEffect(() => {
    loadMore();
  }, []);
  
  // Intersection observer for infinite scroll
  const lastItemRef = useRef();
  useEffect(() => {
    const observer = new IntersectionObserver(
      entries => {
        if (entries[0].isIntersecting && hasMore) {
          loadMore();
        }
      },
      { threshold: 1.0 }
    );
    
    if (lastItemRef.current) {
      observer.observe(lastItemRef.current);
    }
    
    return () => observer.disconnect();
  }, [hasMore]);
  
  return (
    <div className="timeline">
      {items.map((item, index) => (
        <div 
          key={`${item.type}-${item.content.timestamp}-${index}`}
          ref={index === items.length - 1 ? lastItemRef : null}
        >
          <TimelineItem item={item} />
        </div>
      ))}
    </div>
  );
}

Activity Heatmap

Visualize capture density over time:
// Get captures per hour for a day
async function getActivityHeatmap(date: Date) {
  const start = new Date(date.setHours(0, 0, 0, 0));
  const end = new Date(date.setHours(23, 59, 59, 999));
  
  const hours = [];
  for (let hour = 0; hour < 24; hour++) {
    const hourStart = new Date(start.getTime() + hour * 60 * 60 * 1000);
    const hourEnd = new Date(hourStart.getTime() + 60 * 60 * 1000);
    
    const response = await fetch(
      `http://localhost:3030/timeline?` +
      `start_time=${hourStart.toISOString()}&` +
      `end_time=${hourEnd.toISOString()}&` +
      `limit=1`
    );
    const data = await response.json();
    
    hours.push({
      hour,
      count: data.pagination.total
    });
  }
  
  return hours;
}

Timeline Performance

Pagination Strategy

1

Load Initial Page

GET /timeline?limit=50&offset=0
# Returns items 0-49
# Response time: ~20-50ms
2

User Scrolls

GET /timeline?limit=50&offset=50
# Returns items 50-99
# Response time: ~20-50ms (cached if recent)
3

Lazy Load Images

<img loading="lazy" src="/frames/12345?image=true" />
# Only loads when scrolled into view
# Reduces initial page load time
Performance Tips:
  • Use limit=50 for smooth scrolling
  • Add loading="lazy" to frame images
  • Cache timeline responses client-side
  • Use virtual scrolling for 1000+ items

Database Indexing

Timeline queries are optimized with indexes:
CREATE INDEX idx_frames_ts_device ON frames(timestamp, device_name);
CREATE INDEX idx_audio_ts_device ON audio_transcriptions(timestamp, device_name);

-- Query uses index for fast timestamp-based sorting
SELECT * FROM frames 
WHERE timestamp > ? AND timestamp < ?
ORDER BY timestamp DESC
LIMIT 50 OFFSET 0;

Playback Features

Frame-by-Frame Navigation

// Get current frame
const current = await fetch(`/frames/${frameId}`);
const data = await current.json();

// Get next frame (by timestamp)
const next = await fetch(
  `/timeline?` +
  `start_time=${data.timestamp}&` +
  `content_type=ocr&` +
  `limit=1&` +
  `offset=1`
);

// Get previous frame
const prev = await fetch(
  `/timeline?` +
  `end_time=${data.timestamp}&` +
  `content_type=ocr&` +
  `limit=1`
);

Timeline Scrubbing

function TimeScrubber({ startTime, endTime, onSeek }) {
  const [position, setPosition] = useState(0);
  
  const handleScrub = (e) => {
    const rect = e.currentTarget.getBoundingClientRect();
    const x = e.clientX - rect.left;
    const percentage = x / rect.width;
    
    const start = new Date(startTime).getTime();
    const end = new Date(endTime).getTime();
    const targetTime = start + (end - start) * percentage;
    
    onSeek(new Date(targetTime));
  };
  
  return (
    <div 
      className="scrubber"
      onClick={handleScrub}
    >
      <div className="track" />
      <div 
        className="thumb" 
        style={{ left: `${position * 100}%` }}
      />
    </div>
  );
}

Reference

Source files:
  • Timeline API: crates/screenpipe-engine/src/routes/timeline.rs
  • Frame serving: crates/screenpipe-engine/src/routes/frames.rs
  • Database queries: crates/screenpipe-db/src/timeline.rs
  • Video extraction: crates/screenpipe-engine/src/video_utils.rs

Build docs developers (and LLMs) love