Skip to content

Commit 8859c53

Browse files
authored
fix(core): pictureInPicture.enter() assert element and propagate errors to caller
1 parent 54c7513 commit 8859c53

File tree

5 files changed

+122
-58
lines changed

5 files changed

+122
-58
lines changed

projects/core/browser/picture-in-picture/index.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { signal, type Signal, untracked } from '@angular/core';
22
import {
3+
assertElement,
34
constSignal,
45
getPipElement,
56
NOOP_ASYNC_FN,
@@ -30,6 +31,10 @@ export interface PictureInPictureRef {
3031
/**
3132
* Enter Picture-in-Picture mode for the target video element.
3233
*
34+
* @throws {DOMException} `'NotAllowedError'` — the document is not allowed to use PiP
35+
* @throws {DOMException} `'InvalidStateError'` — the video element has `disablePictureInPicture` attribute
36+
* @throws {DOMException} `'NotSupportedError'` — Picture-in-Picture is not supported
37+
*
3338
* @see [HTMLVideoElement: requestPictureInPicture() on MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTMLVideoElement/requestPictureInPicture)
3439
*/
3540
readonly enter: () => Promise<void>;
@@ -98,21 +103,15 @@ export function pictureInPicture(
98103

99104
const enter = async (): Promise<void> => {
100105
const targetEl = toElement.untracked(target);
101-
102-
try {
103-
await targetEl?.requestPictureInPicture();
104-
} catch (error) {
105-
if (ngDevMode) {
106-
console.warn(`[pictureInPicture] Failed to enter Picture-in-Picture mode.`, error);
107-
}
108-
}
106+
assertElement(targetEl, 'pictureInPicture');
107+
await targetEl.requestPictureInPicture();
109108
};
110109

111110
const exit = async (): Promise<void> => {
112111
const targetEl = toElement.untracked(target);
113112
const pipEl = getPipElement(document);
114113

115-
if (targetEl && pipEl && targetEl === pipEl) {
114+
if (targetEl === pipEl) {
116115
try {
117116
await document.exitPictureInPicture();
118117
} catch (error) {

projects/demos/src/common/button.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<button
2+
type="button"
23
class="btn"
34
[class]="'btn--' + variant() + ' btn--' + size()"
45
[disabled]="disabled()"

projects/demos/src/demos/picture-in-picture/picture-in-picture-demo.html

Lines changed: 71 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -21,65 +21,87 @@
2121
</demo-not-supported>
2222
} @else {
2323
<demo-card>
24-
<div class="pip-wrap">
25-
<!-- Hidden functional video -->
26-
<video
27-
#video
28-
class="pip-video"
29-
src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
30-
muted
31-
loop
32-
playsinline
33-
(loadedmetadata)="onVideoLoaded()"
34-
></video>
24+
<div class="pip-wrap" [class.pip-wrap--mobile]="isMobileDevice()">
25+
<!-- Desktop video (hidden) -->
26+
@if (!isMobileDevice()) {
27+
<video
28+
#video
29+
class="pip-video"
30+
src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
31+
muted
32+
loop
33+
playsinline
34+
></video>
35+
}
3536

36-
<!-- Icon ring -->
37-
<div class="pip-visual">
38-
<div class="pip-radar" [class.pip-radar--active]="isActive()">
39-
<div class="pip-radar-ring pip-radar-ring--1"></div>
40-
<div class="pip-radar-ring pip-radar-ring--2"></div>
41-
<div class="pip-radar-ring pip-radar-ring--3"></div>
42-
<div
43-
class="pip-icon-ring"
44-
[class.pip-icon-ring--active]="isActive()"
45-
[class.pip-icon-ring--idle]="!isActive()"
46-
>
47-
<div class="pip-icon-inner">
48-
<svg
49-
width="26"
50-
height="26"
51-
viewBox="0 0 24 24"
52-
fill="none"
53-
stroke="currentColor"
54-
stroke-width="1.5"
55-
stroke-linecap="round"
56-
stroke-linejoin="round"
57-
>
58-
<path d="M11 19H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5" />
59-
<rect class="pip-float-rect" width="9" height="7" x="13" y="13" rx="1" />
60-
</svg>
37+
<!-- Mobile: visible video -->
38+
@if (isMobileDevice()) {
39+
<div class="pip-video-container">
40+
<video
41+
#video
42+
class="pip-video-mobile"
43+
src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
44+
muted
45+
loop
46+
playsinline
47+
preload="metadata"
48+
controls
49+
(playing)="onVideoLoaded()"
50+
(pause)="onVideoLoaded()"
51+
(ended)="onVideoLoaded()"
52+
></video>
53+
</div>
54+
}
55+
56+
<!-- Desktop: Icon ring -->
57+
@if (!isMobileDevice()) {
58+
<div class="pip-visual">
59+
<div class="pip-radar" [class.pip-radar--active]="isActive()">
60+
<div class="pip-radar-ring pip-radar-ring--1"></div>
61+
<div class="pip-radar-ring pip-radar-ring--2"></div>
62+
<div class="pip-radar-ring pip-radar-ring--3"></div>
63+
<div
64+
class="pip-icon-ring"
65+
[class.pip-icon-ring--active]="isActive()"
66+
[class.pip-icon-ring--idle]="!isActive()"
67+
>
68+
<div class="pip-icon-inner">
69+
<svg
70+
width="26"
71+
height="26"
72+
viewBox="0 0 24 24"
73+
fill="none"
74+
stroke="currentColor"
75+
stroke-width="1.5"
76+
stroke-linecap="round"
77+
stroke-linejoin="round"
78+
>
79+
<path d="M11 19H5a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v5" />
80+
<rect class="pip-float-rect" width="9" height="7" x="13" y="13" rx="1" />
81+
</svg>
82+
</div>
6183
</div>
6284
</div>
6385
</div>
64-
</div>
6586

66-
<!-- Text -->
67-
<div class="pip-text">
68-
<h3 class="pip-title">{{ isActive() ? 'Picture-in-Picture' : 'Inactive' }}</h3>
69-
<p class="pip-subtitle">
70-
{{
71-
isActive()
72-
? 'Video is floating in a separate window'
73-
: 'Float the video above other windows'
74-
}}
75-
</p>
76-
</div>
87+
<!-- Text -->
88+
<div class="pip-text">
89+
<h3 class="pip-title">{{ isActive() ? 'Picture-in-Picture' : 'Inactive' }}</h3>
90+
<p class="pip-subtitle">
91+
{{
92+
isActive()
93+
? 'Video is floating in a separate window'
94+
: 'Float the video above other windows'
95+
}}
96+
</p>
97+
</div>
98+
}
7799

78100
<!-- Button -->
79101
<demo-button
80102
variant="secondary"
81103
size="sm"
82-
[disabled]="!videoLoaded()"
104+
[disabled]="isMobileDevice() ? !videoLoaded() : false"
83105
(click)="handleToggle()"
84106
>
85107
{{ isActive() ? 'Exit PiP' : 'Enter PiP' }}

projects/demos/src/demos/picture-in-picture/picture-in-picture-demo.scss

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@
55
padding-top: 0.5rem;
66
padding-bottom: 1.25rem;
77
gap: 0;
8+
9+
&.pip-wrap--mobile {
10+
padding: 0;
11+
}
812
}
913

1014
/* ── Hidden video ── */
@@ -16,6 +20,33 @@
1620
pointer-events: none;
1721
}
1822

23+
.pip-video-mobile {
24+
width: 100%;
25+
height: 100%;
26+
object-fit: contain;
27+
border-radius: 8px;
28+
}
29+
30+
.pip-video-container {
31+
width: 100%;
32+
max-width: 320px;
33+
aspect-ratio: 16 / 9;
34+
min-height: 180px;
35+
background: rgba(255, 255, 255, 0.05);
36+
border-radius: 8px;
37+
margin-bottom: 1rem;
38+
display: flex;
39+
align-items: center;
40+
justify-content: center;
41+
overflow: hidden;
42+
}
43+
44+
.pip-wrap--mobile {
45+
.pip-text {
46+
margin-bottom: 0.75rem;
47+
}
48+
}
49+
1950
/* ── Visual ── */
2051
.pip-visual {
2152
display: flex;

projects/demos/src/demos/picture-in-picture/picture-in-picture-demo.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import { isPlatformBrowser } from '@angular/common';
1212
import { pictureInPicture } from '@signality/core';
1313
import { DemoButton, DemoCard, DemoNotSupported, Wrapper } from '../../common';
1414

15+
const MOBILE_REGEX = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i;
16+
1517
@Component({
1618
selector: 'demo-picture-in-picture',
1719
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -27,9 +29,18 @@ export class PictureInPictureDemo {
2729
readonly pip = pictureInPicture(this.video);
2830
readonly videoLoaded = signal(false);
2931
readonly isActive = this.pip.isActive;
32+
readonly isMobileDevice = signal(false);
3033

3134
constructor() {
35+
effect(() => {
36+
this.isMobileDevice.set(MOBILE_REGEX.test(navigator.userAgent));
37+
});
38+
3239
effect(async () => {
40+
if (this.isMobileDevice()) {
41+
return;
42+
}
43+
3344
const video = this.video()?.nativeElement;
3445
const pipActive = this.isActive;
3546

0 commit comments

Comments
 (0)