@banou/media-player 0.10.6 → 0.10.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -61,6 +61,43 @@ export type DownloadedRange = {
61
61
  endByteOffset: number
62
62
  }
63
63
 
64
+ /** A box in viewport coordinates, as `getBoundingClientRect` reports it. */
65
+ export type ViewportBox = {
66
+ left: number
67
+ top: number
68
+ width: number
69
+ height: number
70
+ }
71
+
72
+ /**
73
+ * The picture in picture control of a media the player does not own, which the host enters from its
74
+ * own document (see `PassThroughPictureInPicture`).
75
+ *
76
+ * Armed means a click on the control goes through it to whatever the host renders below, so the
77
+ * chrome lays a layer over everything else with a hole at `armed`: inside the hole every pointer event
78
+ * belongs to the host's document, and the layer is how this one learns the pointer has left it.
79
+ */
80
+ export type PassThroughControl = {
81
+ /** Between the media's `enterpictureinpicture` and `leavepictureinpicture`. */
82
+ active: boolean
83
+ /** The control's box while a click on it is let through, undefined otherwise. */
84
+ armed?: ViewportBox
85
+ /**
86
+ * Lets a click at `box` through to the host, and tells the host it is armed. For the pointer
87
+ * arriving on the control. Does nothing while the media has no metadata yet (`readyState` below 1)
88
+ * or has an `error`, since the host's request would be refused and the click lost with it.
89
+ */
90
+ arm: (box: ViewportBox) => void
91
+ /**
92
+ * Takes the click back, and tells the host it is disarmed if it had been told it was armed. For the
93
+ * pointer leaving the control; also done here on entering picture in picture, on a touch, and when
94
+ * the control stops being offered.
95
+ */
96
+ disarm: () => void
97
+ /** Leaves picture in picture through the media. */
98
+ exit: () => void
99
+ }
100
+
64
101
  /**
65
102
  * Everything about the source that the chrome reads, carried on the player store next to the built-in
66
103
  * playback state so a component never has to know which of two channels owns a field.
@@ -145,12 +182,17 @@ export type SourceState = {
145
182
  * Owned here, not by `pip`: that watches the media element, and the window holds a mirror.
146
183
  *
147
184
  * null means the control is not offered at all. Locally that is "no element yet". For a media the
148
- * player does not own it means the source cannot do it, and the difference matters: the compositing
149
- * this does needs a local element to draw, and even a source that forwards the request cannot
150
- * report the resulting STATE back, since `document.pictureInPictureElement` is never a proxy. A
151
- * button that toggles nothing and never lights up is worse than no button.
185
+ * player does not own it is always null, because a request made from this document is refused: the
186
+ * compositing this does needs a local element to draw, and the browser wants a gesture in the
187
+ * media's own document. `passThroughPictureInPicture` is that arm's control instead.
152
188
  */
153
189
  togglePictureInPicture: (() => void) | null
190
+ /**
191
+ * The picture in picture control for a media the player does not own, when the host opted in with
192
+ * `pictureInPicture` and the viewer points with something that hovers. null otherwise, which is
193
+ * always the case for a local source.
194
+ */
195
+ passThroughPictureInPicture: PassThroughControl | null
154
196
 
155
197
  /**
156
198
  * Which shape the control takes. `window` opens one. `burn-in` cannot, and instead paints the
@@ -226,6 +268,7 @@ const initialState: SourceState = {
226
268
  setHideUI: () => {},
227
269
  hasMedia: false,
228
270
  togglePictureInPicture: null,
271
+ passThroughPictureInPicture: null,
229
272
  pictureInPictureMode: null,
230
273
  burnedInSubtitles: false,
231
274
  playbackError: null,
@@ -2,7 +2,7 @@
2
2
  import type { ReactNode } from 'react'
3
3
  import type { MediaChapter } from '../engine'
4
4
  import type { DownloadedRange } from './source-feature'
5
- import type { DelegatedTracks, ExternalThumbnails, PlayerMedia } from './media'
5
+ import type { DelegatedTracks, ExternalThumbnails, PassThroughPictureInPicture, PlayerMedia } from './media'
6
6
  import type { ExposePlayerOptions } from '../remote'
7
7
 
8
8
  import { useEffect, useRef, useState } from 'react'
@@ -13,6 +13,7 @@ import { Player, usePlayer } from './player'
13
13
  import { usePlayback } from './hooks/use-playback'
14
14
  import { useSeekThumbnails } from './hooks/use-thumbnails'
15
15
  import { usePictureInPicture } from './hooks/use-picture-in-picture'
16
+ import { usePassThroughPictureInPicture } from './hooks/use-pass-through-picture-in-picture'
16
17
  import Chrome from './components/chrome'
17
18
 
18
19
  /**
@@ -185,6 +186,12 @@ export type MediaPlayerRemoteOptions =
185
186
  /** The source renders these; the player draws the menu and reports the pick. */
186
187
  subtitles?: DelegatedTracks
187
188
  audioTracks?: DelegatedTracks
189
+ /**
190
+ * Offer picture in picture, entered by the host from inside the media's own document. Off by
191
+ * default, and then there is no control at all. See `PassThroughPictureInPicture` for what the
192
+ * host takes on by passing it. Read by value, so it may be written inline.
193
+ */
194
+ pictureInPicture?: PassThroughPictureInPicture
188
195
  }
189
196
 
190
197
  export type MediaPlayerOptions = MediaPlayerLocalOptions | MediaPlayerRemoteOptions
@@ -264,6 +271,10 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
264
271
  mode: pictureInPictureMode,
265
272
  burnedIn: burnedInSubtitles,
266
273
  } = usePictureInPicture(video, subtitleLayer)
274
+ const passThroughPictureInPicture = usePassThroughPictureInPicture(
275
+ remote?.media ?? null,
276
+ remote?.pictureInPicture,
277
+ )
267
278
 
268
279
  // Subscribed rather than read off the store, because it is a no-op until the media element
269
280
  // attaches: when attach swaps in the real setter the identity changes and these publish again.
@@ -295,6 +306,11 @@ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, childr
295
306
  burnedInSubtitles,
296
307
  ])
297
308
 
309
+ // its own effect: it changes on every hover of the control, and nothing else here does
310
+ useEffect(() => {
311
+ setSourceState({ passThroughPictureInPicture })
312
+ }, [setSourceState, passThroughPictureInPicture])
313
+
298
314
  // A delegated track list writes the same store fields the engine writes, so the menus never learn
299
315
  // which arm they are showing. Only the writer differs: here the pick is forwarded to whoever owns
300
316
  // the document, and it renders the result itself.