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
Number of items to return (max 1000).
Start of time range (ISO 8601 or Unix timestamp).
End of time range (ISO 8601 or Unix timestamp).
Filter by content type: all, ocr, audio, ui.
Filter by application name.
Filter by window title (partial match).
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
Recent Activity
Specific Date
Working Hours
# 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"
# Entire day
curl "http://localhost:3030/timeline?start_time=2026-03-08T00:00:00Z&end_time=2026-03-08T23:59:59Z"
# Specific hour
curl "http://localhost:3030/timeline?start_time=2026-03-08T14:00:00Z&end_time=2026-03-08T15:00:00Z"
// 9am - 5pm today
const today = new Date ();
const start = new Date ( today . setHours ( 9 , 0 , 0 , 0 )). toISOString ();
const end = new Date ( today . setHours ( 17 , 0 , 0 , 0 )). toISOString ();
const response = await fetch (
`http://localhost:3030/timeline?start_time= ${ start } &end_time= ${ end } `
);
Frame Deep Links
Every frame has a direct URL for viewing:
Frame Endpoint
Frame Response
Display Frame
# 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
React Example
TimelineItem Component
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:
Fetch Activity Data
Render Heatmap
// 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 ;
}
Load Initial Page
GET /timeline?limit= 50 & offset = 0
# Returns items 0-49
# Response time: ~20-50ms
User Scrolls
GET /timeline?limit= 50 & offset = 50
# Returns items 50-99
# Response time: ~20-50ms (cached if recent)
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
Next/Previous Frame
Keyboard 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