Build a Pure H5 PDF Screen‑Sharing App with PDF.js and WebSocket
This article details a lightweight pure‑HTML5 solution for synchronizing PDF documents across participants using PDF.js for rendering and a FastAPI‑based WebSocket layer for real‑time state broadcasting, covering architecture, workflow, code snippets, and synchronization logic.
Problem Background
Limited budget, small project scale, and modest team expertise often prevent the use of complex streaming solutions for screen‑sharing PDF documents. The implementation relies solely on HTML5, PDF.js and WebSocket to provide a low‑cost, synchronized PDF sharing solution.
Core Technologies
Backend : FastAPI + WebSocket + Python – manages WebSocket connections, maintains meeting state, handles PDF uploads, and broadcasts synchronization commands.
Frontend : PDF.js – parses PDF URLs, renders pages onto a canvas, and supports paging and zooming.
Communication : WebSocket with custom reconnection/heartbeat and debounce/throttle – ensures stable long‑lived connections and avoids performance degradation from frequent command transmission.
Functional Flow
Session initialization : Presenter and viewers join a room identified by a shared ID (e.g., share). The backend pushes an INIT_STATE message containing the current sharing status and participant count. If sharing is already active, viewers immediately load the presenter’s PDF URL and render it.
Presenter uploads PDF : The presenter selects a local PDF, uploads it to the backend, which generates a URL. The presenter previews the document locally; if sharing is active, the URL and initial state are broadcast to all viewers.
Sharing mode operation : Presenter actions (page turn, zoom, scroll) are debounced/throttled and sent as STATE_UPDATE messages. The backend updates the shared state and broadcasts it to all non‑presenter clients, which apply the new page number, zoom level and scroll position.
Closing sharing mode / connection loss : When the presenter disables sharing, the backend broadcasts SYNC_MODE_CHANGED. Viewers unlock their UI, display a waiting message, and the backend cleans up the connection. If the presenter disconnects, sharing mode terminates automatically.
Key Implementation Details
Presenter – toggling sync mode
function toggleSyncMode() {
const newState = !isSyncMode; // toggle
wsManager.send({
type: 'TOGGLE_SYNC',
enabled: newState
});
if (newState && pdfManager.pdfDoc) {
syncState(); // immediately sync current PDF state
}
}Backend handling of TOGGLE_SYNC
elif msg_type == "TOGGLE_SYNC":
# switch sync mode
await manager.toggle_sync_mode(meeting_id, client_id, data.get("enabled", False))toggle_sync_mode method
async def toggle_sync_mode(self, meeting_id: str, client_id: str, enabled: bool):
if self.controllers.get(meeting_id) != client_id:
return False
state = self.states.get(meeting_id)
if state:
state.is_sync_mode = enabled
state.timestamp = datetime.now().timestamp()
await self.broadcast(meeting_id, {
"type": "SYNC_MODE_CHANGED",
"is_sync_mode": enabled,
"state": asdict(state)
})
return TrueSynchronizing presenter state
function syncState() {
if (!wsManager || !pdfManager.pdfDoc) return;
const state = pdfManager.getState();
state.document_url = currentDocumentUrl;
wsManager.send({
type: 'STATE_UPDATE',
state: state // includes page_number, scale, scroll_x, scroll_y, etc.
});
updatePageDisplay(state.page_number, state.total_pages);
updateScaleDisplay(state.scale);
}pdfManager.getState()
getState() {
return {
page_number: this.currentPage,
total_pages: this.totalPages,
scale: this.scale,
scroll_x: this.container.scrollLeft,
scroll_y: this.container.scrollTop
};
}Backend handling of STATE_UPDATE
if msg_type == "STATE_UPDATE":
await manager.update_state(meeting_id, client_id, data.get("state", {}))update_state method
async def update_state(self, meeting_id: str, client_id: str, data: dict):
if self.controllers.get(meeting_id) != client_id:
return False
state = self.states.get(meeting_id)
if not state: return False
for key, value in data.items():
if hasattr(state, key):
setattr(state, key, value)
state.timestamp = datetime.now().timestamp()
if state.is_sync_mode:
await self.broadcast(meeting_id, {
"type": "STATE_UPDATE",
"state": asdict(state)
}, exclude=client_id) # do not send back to presenter
return TrueViewer – handling synchronization messages
wsManager.on('SYNC_MODE_CHANGED', async (data) => {
isSyncMode = data.is_sync_mode;
updateSyncUI(isSyncMode);
if (isSyncMode && data.state) {
if (data.state.document_url && data.state.document_url !== currentDocumentUrl) {
await loadDocument(data.state.document_url);
}
await pdfManager.applyState(data.state);
updatePageDisplay(data.state.page_number, data.state.total_pages);
updateScaleDisplay(data.state.scale);
}
});
wsManager.on('STATE_UPDATE', async (data) => {
if (!isSyncMode) return;
const state = data.state;
if (state.document_url && state.document_url !== currentDocumentUrl) {
await loadDocument(state.document_url);
}
await pdfManager.applyState(state);
updatePageDisplay(state.page_number, state.total_pages);
updateScaleDisplay(state.scale);
});pdfManager.applyState(state)
async applyState(state) {
if (state.scale && state.scale !== this.scale) {
this.scale = state.scale;
}
if (state.page_number && state.page_number !== this.currentPage) {
await this.renderPage(state.page_number);
} else if (state.scale) {
await this.renderPage(this.currentPage);
}
if (this.container) {
if (state.scroll_x !== undefined) this.container.scrollLeft = state.scroll_x;
if (state.scroll_y !== undefined) this.container.scrollTop = state.scroll_y;
}
}Result and Advantages
The implementation transmits only synchronization commands and the PDF URL, resulting in minimal bandwidth consumption. Role permissions are explicit, WebSocket long‑connections guarantee real‑time updates, and the entire solution runs on a pure HTML5 stack without requiring complex media infrastructure.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
xkx's Tech General Store
Code with the left hand, enjoy with the right; a keystroke sweeps away worries.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
