Skip to content

refactor(core)!: unify fullscreen and pip on media capabilities#1469

Merged
luwes merged 3 commits intomainfrom
refactor/presentation-capabilities
Apr 27, 2026
Merged

refactor(core)!: unify fullscreen and pip on media capabilities#1469
luwes merged 3 commits intomainfrom
refactor/presentation-capabilities

Conversation

@luwes
Copy link
Copy Markdown
Collaborator

@luwes luwes commented Apr 25, 2026

Summary

Lift fullscreen and picture-in-picture state onto the media capability interfaces so the host (rather than the presentation utilities) owns "am I in this mode?", and collapse the WebKit branches onto a single webkitSetPresentationMode path.

Changes

  • MediaFullscreenCapability gains isFullscreen + exitFullscreen; MediaPictureInPictureCapability gains isPictureInPicture + exitPictureInPicture. requestPictureInPicture is now Promise<void>.
  • HTMLVideoElementHost implements the new getters/methods, deriving state from document.fullscreenElement / document.pictureInPictureElement and webkitPresentationMode.
  • Drop webkitDisplayingFullscreen, webkitEnterFullscreen, webkitExitFullscreen from the WebKit type and all call sites — webkitSetPresentationMode('fullscreen' | 'inline' | 'picture-in-picture') covers everything.
  • Rename isFullscreenElementisFullscreen and isPictureInPictureElementisPictureInPicture; both now consult the capability getters as a final fallback so non-HTMLMediaElement hosts work.
  • Detect fullscreen via the :fullscreen pseudo-class so requests issued on a descendant inside a shadow tree (e.g. native controls fullscreening the inner <video>) are still recognized.
  • exitFullscreen now requires a media argument so the WebKit and capability fallbacks have a target.
Why

Presentation utilities previously assumed media was an HTMLMediaElement and reached for vendor APIs directly. Pushing the "is this thing in fullscreen / PiP" decision into the host lets custom media targets (mse-shim, react-native, etc.) participate without the utilities special-casing them, and removes the iOS-vs-everything-else fork inside requestFullscreen / exitFullscreen.

Testing

  • New HTMLVideoElementHost unit tests cover isFullscreen / isPictureInPicture across DOM and WebKit paths.
  • Existing fullscreen and pip feature tests updated for the renamed helpers, the new shadow-tree/:fullscreen path, and the host-driven flow.
  • pnpm -F @videojs/core test

Made with Cursor


Note

Medium Risk
Medium risk because fullscreen/PiP detection and enter/exit behavior is refactored across multiple presentation utilities and player features, with new capability requirements and Safari/WebKit path changes that could affect edge-case platform behavior.

Overview
Refactors fullscreen and picture-in-picture to be capability-driven: MediaFullscreenCapability/MediaPictureInPictureCapability now expose isFullscreen/isPictureInPicture plus exitFullscreen/exitPictureInPicture, and HTMLVideoElementHost implements these by checking document.*Element and WebKit webkitPresentationMode.

Presentation helpers are updated to use the new capabilities as a fallback for non-HTMLMediaElement media, and Safari handling is collapsed onto webkitSetPresentationMode (removing webkitEnterFullscreen/webkitExitFullscreen/webkitDisplayingFullscreen). Fullscreen detection is also expanded to use the :fullscreen pseudo-class to handle shadow DOM/descendant fullscreen scenarios.

Player store features (fullscreen, pip, remote-playback) are updated to the renamed helpers (isFullscreen, isPictureInPicture) and to pass media into exitFullscreen, with extensive new/updated tests covering host-based media and the new detection paths.

Reviewed by Cursor Bugbot for commit d421954. Bugbot is set up for automated code reviews on this repo. Configure here.

Extends MediaFullscreenCapability and MediaPictureInPictureCapability
with isFullscreen/isPictureInPicture getters and exitFullscreen/
exitPictureInPicture methods so the host owns presentation state, and
drops the legacy webkitDisplayingFullscreen / webkitEnterFullscreen /
webkitExitFullscreen branches in favor of webkitSetPresentationMode.

Renames isFullscreenElement -> isFullscreen and isPictureInPictureElement
-> isPictureInPicture, and adds :fullscreen pseudo-class detection so
fullscreen is recognized when requested on a descendant inside a shadow
tree.

Made-with: Cursor
@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 25, 2026

Deploy Preview for vjs10-site ready!

Name Link
🔨 Latest commit d421954
🔍 Latest deploy log https://app.netlify.com/projects/vjs10-site/deploys/69efac3042bd0300082763ce
😎 Deploy Preview https://deploy-preview-1469--vjs10-site.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 25, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
v10-sandbox Ready Ready Preview, Comment Apr 27, 2026 6:34pm

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 25, 2026

📦 Bundle Size Report

🎨 @videojs/html — no changes
Presets (7)
Entry Size
/video (default) 28.71 kB
/video (default + hls) 161.30 kB
/video (minimal) 26.18 kB
/video (minimal + hls) 158.75 kB
/audio (default) 26.58 kB
/audio (minimal) 24.16 kB
/background 4.16 kB
Media (8)
Entry Size
/media/background-video 1.04 kB
/media/container 1.72 kB
/media/dash-video 236.58 kB
/media/hls-video 133.86 kB
/media/mux-audio 160.02 kB
/media/mux-video 160.02 kB
/media/native-hls-video 3.86 kB
/media/simple-hls-video 15.89 kB
Players (3)
Entry Size
/video/player 7.00 kB
/audio/player 5.12 kB
/background/player 3.86 kB
Skins (29)
Entry Type Size
/video/minimal-skin.css css 3.50 kB
/video/skin.css css 3.53 kB
/video/minimal-skin js 26.19 kB
/video/minimal-skin.tailwind js 26.40 kB
/video/skin js 28.72 kB
/video/skin.tailwind js 28.78 kB
/audio/minimal-skin.css css 2.54 kB
/audio/skin.css css 2.50 kB
/audio/minimal-skin js 24.12 kB
/audio/minimal-skin.tailwind js 24.32 kB
/audio/skin js 26.57 kB
/audio/skin.tailwind js 26.73 kB
/background/skin.css css 117 B
/background/skin js 1.15 kB
/live-video/minimal-skin.css css 3.50 kB
/live-video/skin.css css 3.53 kB
/live-video/minimal-skin js 25.94 kB
/live-video/minimal-skin.tailwind js 26.03 kB
/live-video/skin js 28.38 kB
/live-video/skin.tailwind js 28.47 kB
/live-audio/minimal-skin.css css 2.54 kB
/live-audio/skin.css css 2.50 kB
/live-audio/minimal-skin js 23.92 kB
/live-audio/minimal-skin.tailwind js 23.93 kB
/live-audio/skin js 26.33 kB
/live-audio/skin.tailwind js 26.37 kB
/base.css css 157 B
/shared.css css 88 B
/skin-element js 1.36 kB
UI Components (25)
Entry Size
/ui/alert-dialog 1.01 kB
/ui/alert-dialog-close 474 B
/ui/alert-dialog-description 407 B
/ui/alert-dialog-title 383 B
/ui/buffering-indicator 2.58 kB
/ui/captions-button 2.68 kB
/ui/cast-button 2.66 kB
/ui/compounds 4.18 kB
/ui/controls 2.34 kB
/ui/error-dialog 3.04 kB
/ui/fullscreen-button 2.64 kB
/ui/hotkey 1.92 kB
/ui/mute-button 2.68 kB
/ui/pip-button 2.68 kB
/ui/play-button 2.65 kB
/ui/playback-rate-button 2.67 kB
/ui/popover 1.84 kB
/ui/poster 2.34 kB
/ui/seek-button 2.65 kB
/ui/slider 1.54 kB
/ui/thumbnail 2.93 kB
/ui/time 2.50 kB
/ui/time-slider 3.89 kB
/ui/tooltip 2.05 kB
/ui/volume-slider 2.70 kB

Sizes are marginal over the root entry point.

⚛️ @videojs/react — no changes
Presets (7)
Entry Size
/video (default) 23.43 kB
/video (default + hls) 154.82 kB
/video (minimal) 21.07 kB
/video (minimal + hls) 152.50 kB
/audio (default) 19.03 kB
/audio (minimal) 17.55 kB
/background 755 B
Media (7)
Entry Size
/media/background-video 575 B
/media/dash-video 235.21 kB
/media/hls-video 132.51 kB
/media/mux-audio 158.71 kB
/media/mux-video 158.69 kB
/media/native-hls-video 2.36 kB
/media/simple-hls-video 14.44 kB
Skins (26)
Entry Type Size
/video/minimal-skin.css css 3.44 kB
/video/skin.css css 3.46 kB
/video/minimal-skin js 20.97 kB
/video/minimal-skin.tailwind js 24.49 kB
/video/skin js 23.35 kB
/video/skin.tailwind js 24.61 kB
/audio/minimal-skin.css css 2.44 kB
/audio/skin.css css 2.39 kB
/audio/minimal-skin js 17.48 kB
/audio/minimal-skin.tailwind js 19.97 kB
/audio/skin js 18.92 kB
/audio/skin.tailwind js 19.94 kB
/background/skin.css css 90 B
/background/skin js 272 B
/live-video/minimal-skin.css css 3.44 kB
/live-video/skin.css css 3.46 kB
/live-video/minimal-skin js 17.70 kB
/live-video/minimal-skin.tailwind js 21.10 kB
/live-video/skin js 20.08 kB
/live-video/skin.tailwind js 21.24 kB
/live-audio/minimal-skin.css css 2.44 kB
/live-audio/skin.css css 2.39 kB
/live-audio/minimal-skin js 15.66 kB
/live-audio/minimal-skin.tailwind js 17.97 kB
/live-audio/skin js 17.16 kB
/live-audio/skin.tailwind js 18.06 kB
UI Components (20)
Entry Size
/ui/alert-dialog 1.10 kB
/ui/buffering-indicator 1.82 kB
/ui/captions-button 2.00 kB
/ui/cast-button 1.94 kB
/ui/controls 1.65 kB
/ui/error-dialog 1.83 kB
/ui/fullscreen-button 1.99 kB
/ui/mute-button 1.97 kB
/ui/pip-button 1.93 kB
/ui/play-button 2.00 kB
/ui/playback-rate-button 1.97 kB
/ui/popover 1.84 kB
/ui/poster 1.62 kB
/ui/seek-button 1.99 kB
/ui/slider 2.65 kB
/ui/thumbnail 2.08 kB
/ui/time 1.41 kB
/ui/time-slider 2.45 kB
/ui/tooltip 2.21 kB
/ui/volume-slider 2.40 kB

Sizes are marginal over the root entry point.

🧩 @videojs/core — no changes
Entries (9)
Entry Size
. 4.96 kB
/dom 11.82 kB
/dom/media/custom-media-element 1.90 kB
/dom/media/dash 234.36 kB
/dom/media/google-cast 4.07 kB
/dom/media/hls 132.09 kB
/dom/media/mux 158.03 kB
/dom/media/native-hls 1.72 kB
/dom/media/simple-hls 13.83 kB
🏷️ @videojs/element — no changes
Entries (2)
Entry Size
. 996 B
/context 943 B
📦 @videojs/store — no changes
Entries (3)
Entry Size
. 1.39 kB
/html 695 B
/react 360 B
🔧 @videojs/utils — no changes
Entries (10)
Entry Size
/array 104 B
/dom 1.92 kB
/events 319 B
/function 327 B
/object 275 B
/predicate 265 B
/string 148 B
/style 190 B
/time 478 B
/number 158 B
📦 @videojs/spf — no changes
Entries (3)
Entry Size
. 40 B
/dom 13.33 kB
/playback-engine 13.24 kB

ℹ️ How to interpret

All sizes are standalone totals (minified + brotli).

Icon Meaning
No change
🔺 Increased ≤ 10%
🔴 Increased > 10%
🔽 Decreased
🆕 New (no baseline)

Run pnpm size locally to check current sizes.

Comment thread packages/core/src/dom/presentation/fullscreen.ts
mihar-22
mihar-22 previously approved these changes Apr 25, 2026
Copy link
Copy Markdown
Member

@mihar-22 mihar-22 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perfect! Thank you for doing this ❤️

Small recommendations and questions below.

Comment thread packages/core/src/dom/presentation/fullscreen.ts Outdated
return this.target?.requestPictureInPicture() ?? Promise.reject();
get isPictureInPicture(): boolean {
return (
(!!this.target && document.pictureInPictureElement === this.target) ||
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this can just be document.pictureInPictureElement === this.target?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if they're both null?

Comment thread packages/core/src/core/media/types.ts Outdated
Comment thread packages/core/src/core/media/types.ts Outdated
Comment thread packages/core/src/dom/media/video-host.ts Outdated
Comment thread packages/core/src/dom/media/video-host.ts Outdated
Comment thread packages/core/src/dom/media/video-host.ts Outdated
Comment thread packages/core/src/dom/media/video-host.ts Outdated
Comment thread packages/core/src/dom/presentation/pip.ts Outdated
- broaden capability return types to Promise<unknown> so adapters can forward native results
- guard webkitSetPresentationMode by current presentation mode before exiting
- use globalThis.document and remove redundant throws so unsupported paths no-op

Made-with: Cursor
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit c4da2bd. Configure here.

Comment thread packages/core/src/dom/store/features/tests/fullscreen.test.ts
The exitFullscreen path now requires webkitPresentationMode === 'fullscreen'
before calling webkitSetPresentationMode('inline'), so the mock must reflect
that state.

Made-with: Cursor
@luwes luwes merged commit bb72a84 into main Apr 27, 2026
26 checks passed
@luwes luwes deleted the refactor/presentation-capabilities branch April 27, 2026 18:59
@luwes luwes mentioned this pull request Apr 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants