A Chrome extension that keeps your Microsoft Teams meeting visible across all tabs using a floating Picture-in-Picture window with full meeting controls.
[Add a screenshot or GIF here showing the floating PiP window over other tabs]
When you're in a Teams meeting and switch to another tab, you lose sight of the call. Unlike Google Meet, Teams doesn't offer a built-in way to keep the meeting visible while working in other tabs.
This extension opens a small floating window that stays on top of everything, showing the active speaker's camera (or screen share) with controls for mute, raise hand, leave, and pin-to-top. You can switch between viewing the camera feed, screen share, and participant list with a single click.
- Always-on-top floating window using Chrome's Document Picture-in-Picture API
- Meeting controls directly in the floating window: mute/unmute, raise/lower hand, leave call
- Pin to top -- keeps the PiP window above all other windows with automatic re-focus
- Live status indicator showing whether you're live or muted
- Meeting title displayed in the PiP window header
- Call duration timer showing elapsed time since joining
- Camera / Screen / Participants toggle to switch between participant video, shared content, and participant list
- Participant count badge when multiple people have their cameras on
- Smart screen share detection with a 3-tier strategy to distinguish screen shares from camera feeds
- SVG icons that update dynamically with mute and hand state
- Keyboard shortcut -- press
Alt+Pto toggle the PiP window from any tab - Auto-open -- optionally opens PiP automatically when you join a meeting
- Options page -- configure PiP window size, default view, and auto-open behavior
- Extension popup with one-click access and status overview
- Works with both Teams for Business (
teams.microsoft.com) and Teams Personal (teams.live.com)
- Google Chrome 116 or later (uses the Document Picture-in-Picture API)
- A Microsoft Teams tab with an active meeting
-
Download or clone this repository:
git clone https://github.com/fafugg/teams-floating-controls.git
-
Open Chrome and navigate to
chrome://extensions/ -
Enable Developer mode (toggle in the top-right corner)
-
Click Load unpacked and select the project folder
-
The extension icon will appear in your toolbar. Pin it for easy access.
-
Join a meeting in Microsoft Teams
-
Click the Teams Floating Controls icon in your toolbar
-
Click Open floating controls
-
A small window will appear showing the active speaker's camera feed with controls at the bottom:
Button Action Microphone Toggle mute / unmute Hand Raise or lower your hand Phone Leave the meeting (with confirmation) Pin Pin the window on top of all other windows Cam / Screen / Participants Switch between camera view, screen share, and participant list -
If you click the extension icon while the floating window is already open, it will raise the floating window to the front. If you're on a different tab, it will switch back to the Teams tab.
-
Press
Alt+Pto toggle the PiP window from any Teams tab. -
Right-click the extension icon and select Options to configure window size, default view, and auto-open behavior.
teams-floating-controls/
├── manifest.json # Extension manifest (Manifest V3)
├── background.js # Service worker -- tracks PiP window ID, keyboard shortcuts, focus
├── content.js # Thin orchestrator -- message routing, autoOpen polling
├── popup.html / popup.js # Extension popup UI
├── options.html / options.js # Settings page (PiP size, auto-open, default view)
├── lib/
│ ├── selectors.js # DOM selectors with fallbacks for Teams UI variants
│ ├── meeting-state.js # Detect mute, hand, and meeting state from the DOM
│ ├── stream-detection.js # Detect video streams, screen shares, dominant speaker
│ ├── pip-ui.js # Build and sync the PiP window UI (icons, title, duration)
│ └── pip-window.js # PiP window lifecycle, MutationObserver-based syncing
├── icons/ # Extension icons (16, 48, 128px PNG + SVG meeting controls)
├── tests/ # Unit tests (Node built-in test runner)
└── pip-styles.css # Styles for the PiP window
Selectors (lib/selectors.js) -- Defines CSS selector arrays with progressive fallbacks for mute, leave, and raise-hand buttons. The find() function tries each selector in order and returns the first match.
Meeting State (lib/meeting-state.js) -- Reads the Teams DOM to determine mute state (muted / unmuted / unknown), hand state (raised / lowered / unknown), and whether the user is in a meeting.
Stream Detection (lib/stream-detection.js) -- Uses a 3-tier strategy to detect video streams and distinguish screen shares from camera feeds. Also identifies the dominant speaker and extracts participant names.
PiP UI (lib/pip-ui.js) -- Builds the PiP window UI by loading CSS and HTML templates. Attaches event listeners for all controls and runs syncPiP() to keep button states, video elements, meeting title, and duration in sync with the actual meeting state.
PiP Window (lib/pip-window.js) -- Manages the Document PiP window lifecycle: opening via documentPictureInPicture.requestWindow(), closing, and setting up a MutationObserver with 5-second safety-net polling to sync DOM changes.
Content Script (content.js) -- Thin orchestrator that routes messages between the background/popup and the lib modules. Also runs auto-open polling to automatically open PiP when a meeting starts (if enabled in settings).
Background Service Worker (background.js) -- Tracks the OS-level window ID of the PiP popup for reliable focus control. Handles keyboard shortcuts (Alt+P) and routes messages between components.
Popup (popup.html / popup.js) -- Finds a Teams tab (active first, then scans all tabs), ensures the content script is injected, queries meeting status, and either opens the PiP window or focuses it if already open.
Options (options.html / options.js) -- Settings page for configuring PiP window dimensions, default view (auto/camera/screen/participants), and auto-open behavior.
Popup --(get-status)------> Content Script --> Teams DOM
Popup --(open-pip)--------> Content Script --> Document PiP API
Popup --(focus-pip-window)> Background -----> chrome.windows.update()
Content Script --(pip-about-to-open)--> Background (tracks window)
Content Script --(pip-window-ready)---> Background (validates window ID)
Content Script --(pip-closed)----------> Background (clears tracking)
Content Script --(get-pip-window-id)---> Background (for pin-to-top)
The extension uses a 3-tier strategy to tell participant cameras apart from screen shares:
-
displaySurfaceAPI -- The browser exposes this onMediaStreamTrack.getSettings()when a local screen capture is active. Most reliable when the local user is sharing. -
DOM ancestry scan -- Walks up the DOM from each
<video>element looking for Teams container markers (aria-label,data-tid, oridattributes containing "screen", "content", or "sharing"). Catches remote screen shares. -
Resolution heuristic -- Fallback: if a video stream is >=1280px wide with an aspect ratio >1.55, it's likely a screen share rather than a camera feed.
Teams frequently updates its UI. Each button has multiple fallback selectors to handle changes:
mute: '[data-tid="toggle-mute"]', 'button[aria-label*="Unmute" i]', 'button[aria-label*="Mute" i]'
leave: '[data-tid="hangup-button"]', 'button[aria-label*="Leave" i]', 'button[aria-label*="End call" i]'
raiseHand: '#raisehands-button', 'button[data-inp="raisehands-button"]', '[data-tid="raise-hand-button"]', ...| Permission | Why it's needed |
|---|---|
activeTab |
Access the current Teams tab to communicate with the content script |
scripting |
Inject content scripts into Teams tabs on demand |
tabs |
Find Teams tabs across all windows |
windows |
Focus the PiP window and its parent Teams window via chrome.windows.update() |
storage |
Persist user settings (PiP size, auto-open, default view) |
host_permissions |
Limit script injection to Teams domains only (teams.microsoft.com, teams.live.com) |
- "No active meeting detected" -- Make sure you've joined a call (not just opened Teams). The extension looks for the mute/leave buttons in the DOM to confirm you're in a meeting.
- "Document Picture-in-Picture requires Chrome 116 or later" -- Update Chrome to the latest version.
- "Cannot reach Teams page" -- Try reloading the Teams tab. The content script needs to be loaded to communicate.
- No video in the floating window -- Ensure at least one participant has their camera on. If no video is available, a "No camera" placeholder is shown.
- Screen share not appearing -- The Screen toggle is disabled when no screen share is detected. It will enable automatically when someone starts sharing.
- Raise hand button not working -- The raise hand button may be collapsed into the Teams toolbar overflow menu. The extension tries multiple selectors but cannot click buttons that aren't rendered in the DOM.
MIT License - do whatever you want, hope it helps