@oxyhq/bloom 1.1.0 → 1.2.1
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.
- package/docs/media-flight.mdx +282 -0
- package/docs/migrating-the-media-gallery.mdx +103 -0
- package/docs/migrating-to-1.0.mdx +1 -1
- package/docs/zoomable-media-gallery.mdx +116 -0
- package/lib/commonjs/image-aspect-ratio-cache/aspect-ratio-cache.js +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/media-flight/MediaFlightLayer.js +151 -0
- package/lib/commonjs/media-flight/MediaFlightLayer.js.map +1 -0
- package/lib/commonjs/media-flight/MediaSurface.js +207 -0
- package/lib/commonjs/media-flight/MediaSurface.js.map +1 -0
- package/lib/commonjs/media-flight/constants.js +39 -0
- package/lib/commonjs/media-flight/constants.js.map +1 -0
- package/lib/commonjs/media-flight/expo-video-module.js +180 -0
- package/lib/commonjs/media-flight/expo-video-module.js.map +1 -0
- package/lib/commonjs/media-flight/index.js +108 -0
- package/lib/commonjs/media-flight/index.js.map +1 -0
- package/lib/commonjs/media-flight/store.js +457 -0
- package/lib/commonjs/media-flight/store.js.map +1 -0
- package/lib/commonjs/media-flight/types.js +6 -0
- package/lib/commonjs/media-flight/types.js.map +1 -0
- package/lib/commonjs/media-flight/use-media-flight.js +41 -0
- package/lib/commonjs/media-flight/use-media-flight.js.map +1 -0
- package/lib/commonjs/tab-bar/TabBarBase.js +1 -1
- package/lib/commonjs/{zoomable-image-gallery/ZoomableImageGallery.js → zoomable-media-gallery/ZoomableMediaGallery.js} +185 -131
- package/lib/commonjs/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -0
- package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.js.map +1 -1
- package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.js +5 -5
- package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.js.map +1 -1
- package/lib/commonjs/zoomable-media-gallery/types.js +6 -0
- package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/types.js.map +1 -1
- package/lib/module/image-aspect-ratio-cache/aspect-ratio-cache.js +1 -1
- package/lib/module/index.js +2 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +2 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/media-flight/MediaFlightLayer.js +146 -0
- package/lib/module/media-flight/MediaFlightLayer.js.map +1 -0
- package/lib/module/media-flight/MediaSurface.js +199 -0
- package/lib/module/media-flight/MediaSurface.js.map +1 -0
- package/lib/module/media-flight/constants.js +35 -0
- package/lib/module/media-flight/constants.js.map +1 -0
- package/lib/module/media-flight/expo-video-module.js +173 -0
- package/lib/module/media-flight/expo-video-module.js.map +1 -0
- package/lib/module/media-flight/index.js +9 -0
- package/lib/module/media-flight/index.js.map +1 -0
- package/lib/module/media-flight/store.js +439 -0
- package/lib/module/media-flight/store.js.map +1 -0
- package/lib/module/media-flight/types.js +4 -0
- package/lib/module/media-flight/types.js.map +1 -0
- package/lib/module/media-flight/use-media-flight.js +36 -0
- package/lib/module/media-flight/use-media-flight.js.map +1 -0
- package/lib/module/tab-bar/TabBarBase.js +1 -1
- package/lib/module/{zoomable-image-gallery/ZoomableImageGallery.js → zoomable-media-gallery/ZoomableMediaGallery.js} +184 -130
- package/lib/module/zoomable-media-gallery/ZoomableMediaGallery.js.map +1 -0
- package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.js.map +1 -1
- package/lib/module/zoomable-media-gallery/index.js +5 -0
- package/lib/module/zoomable-media-gallery/index.js.map +1 -0
- package/lib/module/zoomable-media-gallery/types.js +4 -0
- package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/types.js.map +1 -1
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/media-flight/MediaFlightLayer.d.ts +11 -0
- package/lib/typescript/commonjs/media-flight/MediaFlightLayer.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/MediaSurface.d.ts +106 -0
- package/lib/typescript/commonjs/media-flight/MediaSurface.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/constants.d.ts +18 -0
- package/lib/typescript/commonjs/media-flight/constants.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/expo-video-module.d.ts +125 -0
- package/lib/typescript/commonjs/media-flight/expo-video-module.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/index.d.ts +9 -0
- package/lib/typescript/commonjs/media-flight/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/store.d.ts +132 -0
- package/lib/typescript/commonjs/media-flight/store.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/types.d.ts +195 -0
- package/lib/typescript/commonjs/media-flight/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/media-flight/use-media-flight.d.ts +4 -0
- package/lib/typescript/commonjs/media-flight/use-media-flight.d.ts.map +1 -0
- package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts +5 -0
- package/lib/typescript/commonjs/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -0
- package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-media-gallery/index.d.ts +4 -0
- package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/zoomable-media-gallery/types.d.ts +90 -0
- package/lib/typescript/commonjs/zoomable-media-gallery/types.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/index.web.d.ts.map +1 -1
- package/lib/typescript/module/media-flight/MediaFlightLayer.d.ts +11 -0
- package/lib/typescript/module/media-flight/MediaFlightLayer.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/MediaSurface.d.ts +106 -0
- package/lib/typescript/module/media-flight/MediaSurface.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/constants.d.ts +18 -0
- package/lib/typescript/module/media-flight/constants.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/expo-video-module.d.ts +125 -0
- package/lib/typescript/module/media-flight/expo-video-module.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/index.d.ts +9 -0
- package/lib/typescript/module/media-flight/index.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/store.d.ts +132 -0
- package/lib/typescript/module/media-flight/store.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/types.d.ts +195 -0
- package/lib/typescript/module/media-flight/types.d.ts.map +1 -0
- package/lib/typescript/module/media-flight/use-media-flight.d.ts +4 -0
- package/lib/typescript/module/media-flight/use-media-flight.d.ts.map +1 -0
- package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts +5 -0
- package/lib/typescript/module/zoomable-media-gallery/ZoomableMediaGallery.d.ts.map +1 -0
- package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-media-gallery/index.d.ts +4 -0
- package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/index.d.ts.map +1 -1
- package/lib/typescript/module/zoomable-media-gallery/types.d.ts +90 -0
- package/lib/typescript/module/zoomable-media-gallery/types.d.ts.map +1 -0
- package/package.json +42 -8
- package/src/image-aspect-ratio-cache/aspect-ratio-cache.ts +1 -1
- package/src/index.ts +2 -1
- package/src/index.web.ts +2 -1
- package/src/media-flight/MediaFlightLayer.tsx +144 -0
- package/src/media-flight/MediaSurface.tsx +251 -0
- package/src/media-flight/constants.ts +32 -0
- package/src/media-flight/expo-video-module.ts +212 -0
- package/src/media-flight/index.ts +34 -0
- package/src/media-flight/store.ts +497 -0
- package/src/media-flight/types.ts +210 -0
- package/src/media-flight/use-media-flight.ts +42 -0
- package/src/tab-bar/TabBarBase.tsx +1 -1
- package/src/{zoomable-image-gallery/ZoomableImageGallery.tsx → zoomable-media-gallery/ZoomableMediaGallery.tsx} +192 -117
- package/src/zoomable-media-gallery/index.ts +11 -0
- package/src/zoomable-media-gallery/types.ts +100 -0
- package/docs/zoomable-image-gallery.mdx +0 -66
- package/lib/commonjs/zoomable-image-gallery/ZoomableImageGallery.js.map +0 -1
- package/lib/commonjs/zoomable-image-gallery/types.js +0 -2
- package/lib/module/zoomable-image-gallery/ZoomableImageGallery.js.map +0 -1
- package/lib/module/zoomable-image-gallery/index.js +0 -5
- package/lib/module/zoomable-image-gallery/index.js.map +0 -1
- package/lib/module/zoomable-image-gallery/types.js +0 -2
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts +0 -5
- package/lib/typescript/commonjs/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +0 -1
- package/lib/typescript/commonjs/zoomable-image-gallery/index.d.ts +0 -4
- package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts +0 -56
- package/lib/typescript/commonjs/zoomable-image-gallery/types.d.ts.map +0 -1
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts +0 -5
- package/lib/typescript/module/zoomable-image-gallery/ZoomableImageGallery.d.ts.map +0 -1
- package/lib/typescript/module/zoomable-image-gallery/index.d.ts +0 -4
- package/lib/typescript/module/zoomable-image-gallery/types.d.ts +0 -56
- package/lib/typescript/module/zoomable-image-gallery/types.d.ts.map +0 -1
- package/src/zoomable-image-gallery/index.ts +0 -9
- package/src/zoomable-image-gallery/types.ts +0 -59
- /package/lib/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.js +0 -0
- /package/lib/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.js +0 -0
- /package/lib/typescript/commonjs/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts +0 -0
- /package/lib/typescript/module/{zoomable-image-gallery → zoomable-media-gallery}/constants.d.ts +0 -0
- /package/src/{zoomable-image-gallery → zoomable-media-gallery}/constants.ts +0 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How long `flyTo` will wait for `<MediaFlightLayer>` to commit the surface
|
|
5
|
+
* before letting the caller carry on regardless (ms).
|
|
6
|
+
*
|
|
7
|
+
* The wait is a COMMIT, not a network round-trip: the surface only has to be
|
|
8
|
+
* mounted, because that is what puts it in expo-video's `_mountedVideos` set
|
|
9
|
+
* while the origin is still there — which is the whole mechanism that carries
|
|
10
|
+
* `currentTime` across. One React commit is a frame or two. Painting is a
|
|
11
|
+
* separate, slower thing that the poster covers (see `MediaSurface`), so it is
|
|
12
|
+
* deliberately NOT waited on here: blocking a tap on a decode would trade a
|
|
13
|
+
* problem nobody can see for one everybody can feel.
|
|
14
|
+
*
|
|
15
|
+
* The timeout exists for the case where no layer is mounted at all. Without it
|
|
16
|
+
* the promise never settles and the consumer never navigates — an app that
|
|
17
|
+
* forgot one line at its root would appear to have a dead feed.
|
|
18
|
+
*/
|
|
19
|
+
export const SURFACE_MOUNT_TIMEOUT_MS = 250;
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* There is deliberately NO hand-off timeout.
|
|
23
|
+
*
|
|
24
|
+
* The obvious one — release the flying surface after N seconds if no destination
|
|
25
|
+
* ever claims it — would have to be wrong in one direction or the other, because
|
|
26
|
+
* the layer cannot tell "the destination failed" from "the caller meant the
|
|
27
|
+
* layer to keep painting this". And the failure it would guard against is not
|
|
28
|
+
* actually bad: a destination that never loads leaves the flying surface
|
|
29
|
+
* showing the video, playing, at exactly the rect the destination wanted, over a
|
|
30
|
+
* destination showing its poster. Going back calls `flyBack`, which releases it.
|
|
31
|
+
*
|
|
32
|
+
* `flyBack(id)` and `releaseFlight(id)` are the escape hatches, and they are
|
|
33
|
+
* the caller's to use.
|
|
34
|
+
*/
|
|
35
|
+
//# sourceMappingURL=constants.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["SURFACE_MOUNT_TIMEOUT_MS"],"sourceRoot":"../../../src","sources":["media-flight/constants.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMA,wBAAwB,GAAG,GAAG;;AAE3C;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA","ignoreList":[]}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The optional-peer boundary for `expo-video`.
|
|
5
|
+
*
|
|
6
|
+
* Bloom ships no native code and never will, so `expo-video` cannot be a hard
|
|
7
|
+
* dependency: an app that renders no video must not be made to install a native
|
|
8
|
+
* module. It is therefore an OPTIONAL peer, and a static `import` contradicts
|
|
9
|
+
* that — Metro resolves every static import in the eager graph, so an omitted
|
|
10
|
+
* "optional" peer does not degrade, it aborts the whole bundle with `Unable to
|
|
11
|
+
* resolve module expo-video`, naming a package the app never mentions.
|
|
12
|
+
*
|
|
13
|
+
* The load has to be dynamic in the ONE shape Metro understands: a `require()`
|
|
14
|
+
* of a STRING LITERAL as a DIRECT STATEMENT of a `try` block, which Metro
|
|
15
|
+
* collects as an optional dependency (resolving the real module when installed,
|
|
16
|
+
* writing `null` into the dependency map when it is not, so the failure lands in
|
|
17
|
+
* the `catch` below at evaluation time instead of at build time). One `if` of
|
|
18
|
+
* nesting loses that — `isOptionalDependency` returns at the FIRST BlockStatement
|
|
19
|
+
* it meets — hence the `typeof require` guard sits OUTSIDE the try.
|
|
20
|
+
*
|
|
21
|
+
* `require` may also not exist at all (this file backs the `import` condition
|
|
22
|
+
* too, i.e. an ESM build under Node), hence that guard.
|
|
23
|
+
*
|
|
24
|
+
* A missing video module is invisible — a poster where a video should be looks
|
|
25
|
+
* exactly like a video that has not started — so {@link warnExpoVideoUnavailable}
|
|
26
|
+
* names the package once, in dev.
|
|
27
|
+
*
|
|
28
|
+
* @see connection-status/netinfo.ts — the same boundary for netinfo.
|
|
29
|
+
* @see hooks/haptics-module.ts — the same boundary for expo-haptics.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Declared locally rather than taken from the ambient `NodeRequire`, which
|
|
34
|
+
* returns `any`: `unknown` forces the module handle to be narrowed below instead
|
|
35
|
+
* of leaking an untyped value into the caller.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The slice of `expo-video`'s `VideoPlayer` Bloom depends on.
|
|
40
|
+
*
|
|
41
|
+
* Hand-written rather than `typeof import('expo-video')` so no expo-video type
|
|
42
|
+
* reaches Bloom's emitted declarations — a consumer that skips the optional peer
|
|
43
|
+
* must not inherit a TS7016 from Bloom's own `.d.ts`. The two are kept in step by
|
|
44
|
+
* the assignability check in `__tests__/MediaFlight.test.tsx`, which is excluded
|
|
45
|
+
* from the build and so may name the real package.
|
|
46
|
+
*
|
|
47
|
+
* Bloom never CALLS any of this. **The consumer creates the player, owns it and
|
|
48
|
+
* destroys it**; Bloom only hands the object back to a `VideoView`, which is the
|
|
49
|
+
* whole mechanism that lets a video move between surfaces without restarting
|
|
50
|
+
* (expo-video's own docs: "when the same player is playing in multiple video
|
|
51
|
+
* views"). The three members below exist so the type is not `object` — a
|
|
52
|
+
* structural minimum the real `VideoPlayer` satisfies, and that an arbitrary
|
|
53
|
+
* object does not.
|
|
54
|
+
*
|
|
55
|
+
* `play`/`pause` are declared in METHOD syntax deliberately: `strictFunctionTypes`
|
|
56
|
+
* exempts method parameters from contravariance, which keeps a real
|
|
57
|
+
* `VideoPlayer` assignable here across `@types/react` copies.
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/** How a video is scaled inside its box. Mirrors expo-video's `VideoContentFit`. */
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Android's rendering surface for a video view.
|
|
64
|
+
*
|
|
65
|
+
* `textureView` is what Bloom asks for everywhere, because a `SurfaceView` is
|
|
66
|
+
* composited by the system OUTSIDE the app's view hierarchy: it ignores the
|
|
67
|
+
* parent's clip, corner radius and transform, so a video flying between two
|
|
68
|
+
* rects would paint as an un-rounded rectangle at the wrong place. It also
|
|
69
|
+
* cannot be changed at runtime (expo-video: "This prop should not be changed at
|
|
70
|
+
* runtime"), which is why {@link MediaSurfaceProps.surfaceType} is captured on
|
|
71
|
+
* first render.
|
|
72
|
+
*/
|
|
73
|
+
|
|
74
|
+
/** The slice of `VideoViewProps` Bloom passes. */
|
|
75
|
+
|
|
76
|
+
/** The slice of expo-video's module surface Bloom renders. */
|
|
77
|
+
|
|
78
|
+
/** `undefined` until the first load attempt, then the module or `null`. */
|
|
79
|
+
let videoModule;
|
|
80
|
+
/** Set by {@link provideExpoVideo}; wins over the `require` below. */
|
|
81
|
+
let providedModule;
|
|
82
|
+
/** Why the load failed, quoted verbatim in the dev warning. */
|
|
83
|
+
let unavailableReason = '';
|
|
84
|
+
let hasWarned = false;
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Hand Bloom the expo-video module explicitly.
|
|
88
|
+
*
|
|
89
|
+
* REQUIRED ON ANY ESM WEB BUNDLER, and measured rather than assumed: in a Vite
|
|
90
|
+
* bundle `typeof require` is `undefined`, so the optional-peer `require` below
|
|
91
|
+
* cannot run and video silently degrades to its poster **even when expo-video
|
|
92
|
+
* is installed**. Metro has a real `require` and is unaffected, which is why
|
|
93
|
+
* this went unnoticed — the app that uses this is Metro-built.
|
|
94
|
+
*
|
|
95
|
+
* There is no bundler-agnostic sync alternative: `import()` is async and this is
|
|
96
|
+
* called during render, so making it async would mean the surface renders
|
|
97
|
+
* without video for a frame and then upgrades. Handing the module over is
|
|
98
|
+
* explicit, synchronous, and costs the consumer one line at startup:
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* import * as ExpoVideo from 'expo-video';
|
|
102
|
+
* provideExpoVideo(ExpoVideo);
|
|
103
|
+
* ```
|
|
104
|
+
*
|
|
105
|
+
* Pass `null` to clear it.
|
|
106
|
+
*/
|
|
107
|
+
export function provideExpoVideo(module) {
|
|
108
|
+
providedModule = module;
|
|
109
|
+
// Drop any cached `require` result so the next load reflects this decision.
|
|
110
|
+
videoModule = undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The expo-video module, or `null` when the optional peer is not installed. */
|
|
114
|
+
export function loadExpoVideo() {
|
|
115
|
+
if (providedModule !== undefined && providedModule !== null) return providedModule;
|
|
116
|
+
if (videoModule !== undefined) return videoModule;
|
|
117
|
+
videoModule = null;
|
|
118
|
+
|
|
119
|
+
// The `typeof require` guard sits OUTSIDE the try on purpose — see the header.
|
|
120
|
+
// Nested one `if`/`else` deeper, the require below is no longer a direct
|
|
121
|
+
// statement of the try block, Metro stops marking it optional, and an app
|
|
122
|
+
// without expo-video fails to BUILD instead of losing the video surface.
|
|
123
|
+
if (typeof require === 'undefined') {
|
|
124
|
+
unavailableReason = 'this bundle has no CommonJS `require`';
|
|
125
|
+
return videoModule;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
const loaded = require('expo-video');
|
|
129
|
+
// `VideoView` is a class component upstream, so `typeof` is `'function'`
|
|
130
|
+
// either way; a memo/forwardRef build would be an object carrying
|
|
131
|
+
// `$$typeof`. Both are accepted, nothing else is.
|
|
132
|
+
const resolved = isComponentBearing(loaded) ? loaded : loaded?.default;
|
|
133
|
+
if (isComponentBearing(resolved)) {
|
|
134
|
+
videoModule = {
|
|
135
|
+
VideoView: resolved.VideoView
|
|
136
|
+
};
|
|
137
|
+
} else {
|
|
138
|
+
unavailableReason = 'the module resolved without a `VideoView` export';
|
|
139
|
+
}
|
|
140
|
+
} catch (error) {
|
|
141
|
+
unavailableReason = error instanceof Error ? error.message : String(error);
|
|
142
|
+
}
|
|
143
|
+
return videoModule;
|
|
144
|
+
}
|
|
145
|
+
function isComponentBearing(candidate) {
|
|
146
|
+
const view = candidate?.VideoView;
|
|
147
|
+
if (typeof view === 'function') return true;
|
|
148
|
+
return typeof view === 'object' && view !== null && '$$typeof' in view;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* One warning per module lifetime, and none in production — the same mechanism
|
|
153
|
+
* as `connection-status/netinfo.ts` and `hooks/haptics-module.ts`. Metro and
|
|
154
|
+
* Vite/Rolldown both fold the `NODE_ENV` check statically, so a production
|
|
155
|
+
* bundle keeps neither the branch nor the message.
|
|
156
|
+
*/
|
|
157
|
+
export function warnExpoVideoUnavailable() {
|
|
158
|
+
if (process.env.NODE_ENV === 'production' || hasWarned) return;
|
|
159
|
+
hasWarned = true;
|
|
160
|
+
// Internal Bloom diagnostic: only the consumer's package.json can fix this,
|
|
161
|
+
// so it names the package, the install command and what is lost.
|
|
162
|
+
// eslint-disable-next-line no-console
|
|
163
|
+
console.warn('[Bloom] A video media surface fell back to its poster: the optional peer ' + '`expo-video` could not be loaded, so no video is rendered and the shared ' + 'media transition carries a still frame instead. Install it ' + '(`npx expo install expo-video`) or pass image content only. ' + `Reason: ${unavailableReason}`);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Test seam — drops the cached module handle so a suite can load it again. */
|
|
167
|
+
export function resetExpoVideoModule() {
|
|
168
|
+
videoModule = undefined;
|
|
169
|
+
providedModule = undefined;
|
|
170
|
+
unavailableReason = '';
|
|
171
|
+
hasWarned = false;
|
|
172
|
+
}
|
|
173
|
+
//# sourceMappingURL=expo-video-module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["videoModule","providedModule","unavailableReason","hasWarned","provideExpoVideo","module","undefined","loadExpoVideo","require","loaded","resolved","isComponentBearing","default","VideoView","error","Error","message","String","candidate","view","warnExpoVideoUnavailable","process","env","NODE_ENV","console","warn","resetExpoVideoModule"],"sourceRoot":"../../../src","sources":["media-flight/expo-video-module.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAIA;AACA;AACA;AACA;AACA;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAOA;;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAGA;;AAkBA;;AAKA;AACA,IAAIA,WAA6C;AACjD;AACA,IAAIC,cAAgD;AACpD;AACA,IAAIC,iBAAiB,GAAG,EAAE;AAC1B,IAAIC,SAAS,GAAG,KAAK;;AAErB;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,gBAAgBA,CAACC,MAA4B,EAAQ;EACnEJ,cAAc,GAAGI,MAAM;EACvB;EACAL,WAAW,GAAGM,SAAS;AACzB;;AAEA;AACA,OAAO,SAASC,aAAaA,CAAA,EAAyB;EACpD,IAAIN,cAAc,KAAKK,SAAS,IAAIL,cAAc,KAAK,IAAI,EAAE,OAAOA,cAAc;EAClF,IAAID,WAAW,KAAKM,SAAS,EAAE,OAAON,WAAW;EACjDA,WAAW,GAAG,IAAI;;EAElB;EACA;EACA;EACA;EACA,IAAI,OAAOQ,OAAO,KAAK,WAAW,EAAE;IAClCN,iBAAiB,GAAG,uCAAuC;IAC3D,OAAOF,WAAW;EACpB;EAEA,IAAI;IACF,MAAMS,MAAM,GAAGD,OAAO,CAAC,YAAY,CAGtB;IACb;IACA;IACA;IACA,MAAME,QAAQ,GAAGC,kBAAkB,CAACF,MAAM,CAAC,GAAGA,MAAM,GAAGA,MAAM,EAAEG,OAAO;IAEtE,IAAID,kBAAkB,CAACD,QAAQ,CAAC,EAAE;MAChCV,WAAW,GAAG;QAAEa,SAAS,EAAEH,QAAQ,CAACG;MAA+C,CAAC;IACtF,CAAC,MAAM;MACLX,iBAAiB,GAAG,kDAAkD;IACxE;EACF,CAAC,CAAC,OAAOY,KAAK,EAAE;IACdZ,iBAAiB,GAAGY,KAAK,YAAYC,KAAK,GAAGD,KAAK,CAACE,OAAO,GAAGC,MAAM,CAACH,KAAK,CAAC;EAC5E;EAEA,OAAOd,WAAW;AACpB;AAEA,SAASW,kBAAkBA,CACzBO,SAA8E,EACU;EACxF,MAAMC,IAAI,GAAGD,SAAS,EAAEL,SAAS;EACjC,IAAI,OAAOM,IAAI,KAAK,UAAU,EAAE,OAAO,IAAI;EAC3C,OAAO,OAAOA,IAAI,KAAK,QAAQ,IAAIA,IAAI,KAAK,IAAI,IAAI,UAAU,IAAIA,IAAI;AACxE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,wBAAwBA,CAAA,EAAS;EAC/C,IAAIC,OAAO,CAACC,GAAG,CAACC,QAAQ,KAAK,YAAY,IAAIpB,SAAS,EAAE;EACxDA,SAAS,GAAG,IAAI;EAChB;EACA;EACA;EACAqB,OAAO,CAACC,IAAI,CACV,2EAA2E,GACzE,2EAA2E,GAC3E,6DAA6D,GAC7D,8DAA8D,GAC9D,WAAWvB,iBAAiB,EAChC,CAAC;AACH;;AAEA;AACA,OAAO,SAASwB,oBAAoBA,CAAA,EAAS;EAC3C1B,WAAW,GAAGM,SAAS;EACvBL,cAAc,GAAGK,SAAS;EAC1BJ,iBAAiB,GAAG,EAAE;EACtBC,SAAS,GAAG,KAAK;AACnB","ignoreList":[]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
export { MediaFlightLayer } from "./MediaFlightLayer.js";
|
|
4
|
+
export { MediaSurface, MediaPoster, EmptyMediaSurface } from "./MediaSurface.js";
|
|
5
|
+
export { useMediaFlight } from "./use-media-flight.js";
|
|
6
|
+
export { DEFAULT_FLIGHT_CORNER_RADIUS, flightProgress, handOffFlight, hasFlight, releaseFlight, resetMediaFlight } from "./store.js";
|
|
7
|
+
export { SURFACE_MOUNT_TIMEOUT_MS } from "./constants.js";
|
|
8
|
+
export { loadExpoVideo, warnExpoVideoUnavailable, provideExpoVideo, resetExpoVideoModule } from "./expo-video-module.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["MediaFlightLayer","MediaSurface","MediaPoster","EmptyMediaSurface","useMediaFlight","DEFAULT_FLIGHT_CORNER_RADIUS","flightProgress","handOffFlight","hasFlight","releaseFlight","resetMediaFlight","SURFACE_MOUNT_TIMEOUT_MS","loadExpoVideo","warnExpoVideoUnavailable","provideExpoVideo","resetExpoVideoModule"],"sourceRoot":"../../../src","sources":["media-flight/index.ts"],"mappings":";;AAAA,SAASA,gBAAgB,QAAQ,uBAAoB;AACrD,SAASC,YAAY,EAAEC,WAAW,EAAEC,iBAAiB,QAAQ,mBAAgB;AAC7E,SAASC,cAAc,QAAQ,uBAAoB;AACnD,SACEC,4BAA4B,EAC5BC,cAAc,EACdC,aAAa,EACbC,SAAS,EACTC,aAAa,EACbC,gBAAgB,QACX,YAAS;AAChB,SAASC,wBAAwB,QAAQ,gBAAa;AACtD,SACEC,aAAa,EACbC,wBAAwB,EACxBC,gBAAgB,EAChBC,oBAAoB,QAMf,wBAAqB","ignoreList":[]}
|
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The media-flight registry: anchors, live surfaces, and the animation that
|
|
5
|
+
* moves them.
|
|
6
|
+
*
|
|
7
|
+
* ## Why the animation lives HERE and not in the layer
|
|
8
|
+
*
|
|
9
|
+
* A flight is started by an imperative call (`flyTo`) from a screen that may be
|
|
10
|
+
* about to unmount. If the layer drove the animation from an effect keyed on the
|
|
11
|
+
* flight record, the first frame would be one commit late — and on web, where
|
|
12
|
+
* the origin route is genuinely gone by the next commit, that frame is the one
|
|
13
|
+
* the user sees. Driving the shared value at the call site makes the leg start
|
|
14
|
+
* exactly when the caller says it does and leaves the layer a pure painter.
|
|
15
|
+
*
|
|
16
|
+
* The shared values are created with `makeMutable` rather than `useSharedValue`
|
|
17
|
+
* for the same reason: they belong to a flight, not to a component, and the
|
|
18
|
+
* component that paints a flight is not the one that starts it.
|
|
19
|
+
*
|
|
20
|
+
* ## Why the registry is anchored on `globalThis`
|
|
21
|
+
*
|
|
22
|
+
* Same reason as the portal group and the overlay stack: `exports` ships a
|
|
23
|
+
* `react-native` → `src` condition beside the `lib/module` and `lib/commonjs`
|
|
24
|
+
* forks, and a bundler can resolve `@oxyhq/bloom/media-flight` through different
|
|
25
|
+
* conditions from different call sites. Two physical copies of this module would
|
|
26
|
+
* each keep their own registry, and the `<MediaFlightLayer>` mounted at the app
|
|
27
|
+
* root would never see the flight a screen started.
|
|
28
|
+
*/
|
|
29
|
+
import { Platform } from 'react-native';
|
|
30
|
+
import { Easing, makeMutable, runOnJS, withSpring, withTiming } from 'react-native-reanimated';
|
|
31
|
+
import { RADIUS } from "../design-tokens/index.js";
|
|
32
|
+
import { SURFACE_MOUNT_TIMEOUT_MS } from "./constants.js";
|
|
33
|
+
import { CLOSE_DURATION_WEB, CLOSE_SPRING, OPEN_DURATION_WEB, OPEN_SPRING } from "../zoomable-media-gallery/constants.js";
|
|
34
|
+
/**
|
|
35
|
+
* Default corner radius of a flying box — Bloom's `radius-12`, the same value
|
|
36
|
+
* the media gallery fits its media at, so a flight that lands in the gallery
|
|
37
|
+
* lands on an identical shape.
|
|
38
|
+
*/
|
|
39
|
+
export const DEFAULT_FLIGHT_CORNER_RADIUS = RADIUS['radius-12'];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Bookkeeping the layer does not RENDER, kept out of the `MediaFlight` record
|
|
43
|
+
* on purpose.
|
|
44
|
+
*
|
|
45
|
+
* The record is replaced on every visual change so its identity tells React the
|
|
46
|
+
* leg moved; folding these flags into it would churn that identity on events
|
|
47
|
+
* nobody paints, and folding the record into these would put mutable state back
|
|
48
|
+
* in a rendered position. They are separate because they answer to different
|
|
49
|
+
* consumers.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
function emptyRegistry() {
|
|
53
|
+
return {
|
|
54
|
+
anchors: new Map(),
|
|
55
|
+
flights: new Map(),
|
|
56
|
+
status: new Map(),
|
|
57
|
+
listeners: new Set(),
|
|
58
|
+
snapshot: [],
|
|
59
|
+
progress: makeMutable(0)
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function statusFor(id) {
|
|
63
|
+
const reg = registry();
|
|
64
|
+
let status = reg.status.get(id);
|
|
65
|
+
if (!status) {
|
|
66
|
+
status = {
|
|
67
|
+
mounted: false,
|
|
68
|
+
settled: false,
|
|
69
|
+
handedOff: false,
|
|
70
|
+
mountWaiters: [],
|
|
71
|
+
mountTimer: null
|
|
72
|
+
};
|
|
73
|
+
reg.status.set(id, status);
|
|
74
|
+
}
|
|
75
|
+
return status;
|
|
76
|
+
}
|
|
77
|
+
function clearTimers(status) {
|
|
78
|
+
if (status.mountTimer !== null) clearTimeout(status.mountTimer);
|
|
79
|
+
status.mountTimer = null;
|
|
80
|
+
}
|
|
81
|
+
function resolveMountWaiters(status) {
|
|
82
|
+
const waiters = status.mountWaiters;
|
|
83
|
+
status.mountWaiters = [];
|
|
84
|
+
if (status.mountTimer !== null) {
|
|
85
|
+
clearTimeout(status.mountTimer);
|
|
86
|
+
status.mountTimer = null;
|
|
87
|
+
}
|
|
88
|
+
for (const waiter of waiters) waiter();
|
|
89
|
+
}
|
|
90
|
+
function registry() {
|
|
91
|
+
globalThis.__oxyhq_bloom_media_flight__ ??= emptyRegistry();
|
|
92
|
+
return globalThis.__oxyhq_bloom_media_flight__;
|
|
93
|
+
}
|
|
94
|
+
function publish(reg) {
|
|
95
|
+
reg.snapshot = [...reg.flights.values()];
|
|
96
|
+
for (const listener of reg.listeners) listener();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** Subscribe to the live-surface set. The layer's `useSyncExternalStore` half. */
|
|
100
|
+
export function subscribeToFlights(listener) {
|
|
101
|
+
const reg = registry();
|
|
102
|
+
reg.listeners.add(listener);
|
|
103
|
+
return () => {
|
|
104
|
+
reg.listeners.delete(listener);
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The live surfaces, oldest first. Stable identity between changes. */
|
|
109
|
+
export function getFlights() {
|
|
110
|
+
return registry().snapshot;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The shared progress value of the most recent leg. */
|
|
114
|
+
export function flightProgress() {
|
|
115
|
+
return registry().progress;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Register (or clear) the node that anchors `id`.
|
|
120
|
+
*
|
|
121
|
+
* A `Map` re-insertion moves the key to the end, which is what makes "the most
|
|
122
|
+
* recently registered node is the anchor" true even when an origin screen's
|
|
123
|
+
* callback ref fires again after the destination has registered.
|
|
124
|
+
*/
|
|
125
|
+
export function registerAnchor(id, node) {
|
|
126
|
+
const reg = registry();
|
|
127
|
+
reg.anchors.delete(id);
|
|
128
|
+
if (node !== null) reg.anchors.set(id, node);
|
|
129
|
+
}
|
|
130
|
+
export function measureAnchor(id) {
|
|
131
|
+
const node = registry().anchors.get(id);
|
|
132
|
+
if (!node) return Promise.resolve(null);
|
|
133
|
+
return new Promise(resolve => {
|
|
134
|
+
node.measureInWindow((x, y, width, height) => {
|
|
135
|
+
// A zero-area measurement is what a node reports while it is laid out but
|
|
136
|
+
// not yet on screen. Flying to it would collapse the surface to a point,
|
|
137
|
+
// so it reads as "no anchor" and the caller skips the transition.
|
|
138
|
+
resolve(width > 0 && height > 0 ? {
|
|
139
|
+
x,
|
|
140
|
+
y,
|
|
141
|
+
width,
|
|
142
|
+
height
|
|
143
|
+
} : null);
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** Whether a surface is currently live for `id`. */
|
|
149
|
+
export function hasFlight(id) {
|
|
150
|
+
return registry().flights.has(id);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Take the surface for `id` live (if it is not already) and move it to `rect`.
|
|
155
|
+
*
|
|
156
|
+
* Retargeting mid-flight keeps the SAME `MediaFlight` record — and therefore the
|
|
157
|
+
* same mounted `VideoView` — which is the whole point. A second record would be
|
|
158
|
+
* a second surface for one id, and swapping between them is exactly the remount
|
|
159
|
+
* that restarts playback.
|
|
160
|
+
*
|
|
161
|
+
* ## Why it returns a promise, and what the promise means
|
|
162
|
+
*
|
|
163
|
+
* It resolves once the layer has COMMITTED a surface for this id — not once
|
|
164
|
+
* that surface has painted.
|
|
165
|
+
*
|
|
166
|
+
* The distinction is the whole ordering contract. expo-video's web player keeps
|
|
167
|
+
* a `Set` of mounted `<video>` elements and, on each mount, copies
|
|
168
|
+
* `currentTime`, play state, volume, muted and rate from `[...set][0]`. If the
|
|
169
|
+
* set is EMPTY when the new element mounts, it returns early and the element
|
|
170
|
+
* starts at zero — a restart. So the layer's surface has to enter that set while
|
|
171
|
+
* the origin's is still in it, which means the caller must not tear the origin
|
|
172
|
+
* down (on web: must not navigate) until this promise resolves.
|
|
173
|
+
*
|
|
174
|
+
* That knowledge lives here rather than in the caller, which is the point: a
|
|
175
|
+
* consumer that has to know the mount order is a consumer that will get it wrong
|
|
176
|
+
* once and produce an intermittent restart nobody can reproduce locally.
|
|
177
|
+
*
|
|
178
|
+
* Painting is deliberately NOT waited on. `MediaSurface` renders the poster
|
|
179
|
+
* BEHIND the video on both platforms, and neither an unpainted `<video>` (no
|
|
180
|
+
* `poster` attribute is set) nor an Android `TextureView` with the ExoPlayer
|
|
181
|
+
* shutter off draws anything opaque — so the still shows through until the first
|
|
182
|
+
* frame lands, and there is no black box to hide. Waiting for a decode would
|
|
183
|
+
* trade an invisible problem for a laggy tap.
|
|
184
|
+
*/
|
|
185
|
+
export function flyTo(id, rect, content, options) {
|
|
186
|
+
const reg = registry();
|
|
187
|
+
const existing = reg.flights.get(id);
|
|
188
|
+
if (existing) {
|
|
189
|
+
// A retarget REPLACES the record rather than mutating it. Identity is what
|
|
190
|
+
// tells React (and the React Compiler) that the leg changed: a mutated
|
|
191
|
+
// record has the same identity, so a memoized read of `flight.from` would
|
|
192
|
+
// keep returning the previous leg's origin forever — the exact
|
|
193
|
+
// external-mutable-state stale read the compiler is documented to produce.
|
|
194
|
+
// The shared value is carried over untouched, so the surface resumes rather
|
|
195
|
+
// than restarting.
|
|
196
|
+
const next = {
|
|
197
|
+
...existing,
|
|
198
|
+
// The leg that was running is abandoned where it stands: `from` becomes
|
|
199
|
+
// the rect the surface is CURRENTLY at, so a retarget never snaps.
|
|
200
|
+
from: interpolateRect(existing.from, existing.to, existing.progress.value),
|
|
201
|
+
to: rect,
|
|
202
|
+
content,
|
|
203
|
+
cornerRadius: options?.cornerRadius ?? existing.cornerRadius,
|
|
204
|
+
contentFit: options?.contentFit ?? existing.contentFit,
|
|
205
|
+
// NOT retargetable: `MediaSurface` captures it on first render because
|
|
206
|
+
// expo-video forbids changing it at runtime, so a new value here would be
|
|
207
|
+
// silently ignored by the view it is meant to configure.
|
|
208
|
+
surfaceType: existing.surfaceType,
|
|
209
|
+
landing: false,
|
|
210
|
+
generation: existing.generation + 1
|
|
211
|
+
};
|
|
212
|
+
reg.flights.set(id, next);
|
|
213
|
+
const retargeted = statusFor(id);
|
|
214
|
+
retargeted.settled = false;
|
|
215
|
+
animate(next, false);
|
|
216
|
+
publish(reg);
|
|
217
|
+
return awaitSurfaceMount(id);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Nothing live yet. `from` is the origin rect the caller measured; without one
|
|
221
|
+
// the surface simply appears at `rect` rather than flying in from a corner.
|
|
222
|
+
const flight = {
|
|
223
|
+
id,
|
|
224
|
+
content,
|
|
225
|
+
from: options?.from ?? rect,
|
|
226
|
+
to: rect,
|
|
227
|
+
cornerRadius: options?.cornerRadius ?? DEFAULT_FLIGHT_CORNER_RADIUS,
|
|
228
|
+
contentFit: options?.contentFit ?? 'cover',
|
|
229
|
+
surfaceType: options?.surfaceType ?? 'textureView',
|
|
230
|
+
progress: makeMutable(0),
|
|
231
|
+
landing: false,
|
|
232
|
+
generation: 0
|
|
233
|
+
};
|
|
234
|
+
reg.flights.set(id, flight);
|
|
235
|
+
const status = statusFor(id);
|
|
236
|
+
status.mounted = false;
|
|
237
|
+
status.settled = false;
|
|
238
|
+
status.handedOff = false;
|
|
239
|
+
animate(flight, false);
|
|
240
|
+
publish(reg);
|
|
241
|
+
return awaitSurfaceMount(id);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Resolve once `<MediaFlightLayer>` reports it has committed the surface, or
|
|
246
|
+
* after {@link SURFACE_MOUNT_TIMEOUT_MS} — which is what happens when no layer
|
|
247
|
+
* is mounted at all. Degrading to "carry on without a transition" is right;
|
|
248
|
+
* never resolving would mean an app that forgot one line at its root has a feed
|
|
249
|
+
* whose videos cannot be opened.
|
|
250
|
+
*/
|
|
251
|
+
function awaitSurfaceMount(id) {
|
|
252
|
+
const status = statusFor(id);
|
|
253
|
+
if (status.mounted) return Promise.resolve();
|
|
254
|
+
return new Promise(resolve => {
|
|
255
|
+
status.mountWaiters.push(resolve);
|
|
256
|
+
if (status.mountTimer !== null) return;
|
|
257
|
+
status.mountTimer = setTimeout(() => {
|
|
258
|
+
status.mountTimer = null;
|
|
259
|
+
warnNoLayerMounted();
|
|
260
|
+
resolveMountWaiters(status);
|
|
261
|
+
}, SURFACE_MOUNT_TIMEOUT_MS);
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The layer calls this from the surface's mount effect. It is the commit signal
|
|
267
|
+
* `flyTo` waits on, and the only thing that makes the ordering contract above
|
|
268
|
+
* true rather than hopeful.
|
|
269
|
+
*/
|
|
270
|
+
export function notifySurfaceMounted(id) {
|
|
271
|
+
const status = statusFor(id);
|
|
272
|
+
status.mounted = true;
|
|
273
|
+
resolveMountWaiters(status);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** The layer calls this when a leg's animation finishes. */
|
|
277
|
+
export function notifySurfaceSettled(id) {
|
|
278
|
+
const status = statusFor(id);
|
|
279
|
+
status.settled = true;
|
|
280
|
+
if (status.handedOff) releaseFlight(id);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* A destination surface reporting that it is LIVE — it has presented a frame of
|
|
285
|
+
* its own, so the flying copy is now redundant and can go.
|
|
286
|
+
*
|
|
287
|
+
* This is the release condition, and it is deliberately not the animation's
|
|
288
|
+
* clock. Measured against production, a fullscreen video route needs ~1 s from
|
|
289
|
+
* tap to first presented frame; releasing when a ~300 ms animation ends would
|
|
290
|
+
* leave ~700 ms with nothing live on screen — the hole the transition exists to
|
|
291
|
+
* cover, made longer.
|
|
292
|
+
*
|
|
293
|
+
* Wire it by giving the destination's `<MediaSurface>` a `flightId`; it calls
|
|
294
|
+
* this itself on its first frame. A destination that renders something other
|
|
295
|
+
* than a Bloom surface can call it directly.
|
|
296
|
+
*/
|
|
297
|
+
export function handOffFlight(id) {
|
|
298
|
+
const reg = registry();
|
|
299
|
+
if (!reg.flights.has(id)) return;
|
|
300
|
+
const status = statusFor(id);
|
|
301
|
+
status.handedOff = true;
|
|
302
|
+
// Mid-flight the surface is still travelling: releasing now would make it
|
|
303
|
+
// vanish somewhere between the two rects. `notifySurfaceSettled` picks it up.
|
|
304
|
+
if (status.settled) releaseFlight(id);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Aim the live surface at its anchor and release it once it lands.
|
|
309
|
+
*
|
|
310
|
+
* With no measurable anchor there is nothing to fly to, so the surface is
|
|
311
|
+
* released immediately — whoever owns the media renders its own copy again at
|
|
312
|
+
* that point, and holding a flying copy over it would double the video.
|
|
313
|
+
*/
|
|
314
|
+
export function flyBack(id) {
|
|
315
|
+
const flight = registry().flights.get(id);
|
|
316
|
+
if (!flight) return;
|
|
317
|
+
void measureAnchor(id).then(rect => {
|
|
318
|
+
// Re-read: the flight may have been released or retargeted while the
|
|
319
|
+
// measurement was outstanding (`measureInWindow` is asynchronous on both
|
|
320
|
+
// platforms), and landing a stale record would fly the wrong surface.
|
|
321
|
+
const reg = registry();
|
|
322
|
+
if (reg.flights.get(id) !== flight) return;
|
|
323
|
+
if (rect === null) {
|
|
324
|
+
releaseFlight(id);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
const landing = {
|
|
328
|
+
...flight,
|
|
329
|
+
from: interpolateRect(flight.from, flight.to, flight.progress.value),
|
|
330
|
+
to: rect,
|
|
331
|
+
landing: true,
|
|
332
|
+
generation: flight.generation + 1
|
|
333
|
+
};
|
|
334
|
+
reg.flights.set(id, landing);
|
|
335
|
+
animate(landing, true);
|
|
336
|
+
publish(reg);
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/** Drop the surface for `id`. Called when a landing leg settles, and on teardown. */
|
|
341
|
+
export function releaseFlight(id) {
|
|
342
|
+
const reg = registry();
|
|
343
|
+
const status = reg.status.get(id);
|
|
344
|
+
if (status) {
|
|
345
|
+
// A waiter still parked on `flyTo`'s promise would otherwise hang until its
|
|
346
|
+
// own timeout, holding a caller that has nothing left to wait for.
|
|
347
|
+
clearTimers(status);
|
|
348
|
+
resolveMountWaiters(status);
|
|
349
|
+
reg.status.delete(id);
|
|
350
|
+
}
|
|
351
|
+
if (!reg.flights.delete(id)) return;
|
|
352
|
+
publish(reg);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Test seam — drops every anchor and surface. */
|
|
356
|
+
export function resetMediaFlight() {
|
|
357
|
+
const existing = globalThis.__oxyhq_bloom_media_flight__;
|
|
358
|
+
// Timers outlive a registry swap: an unfired mount timeout from the previous
|
|
359
|
+
// suite would warn — and resolve a waiter nobody is holding — inside the next.
|
|
360
|
+
if (existing) for (const status of existing.status.values()) clearTimers(status);
|
|
361
|
+
globalThis.__oxyhq_bloom_media_flight__ = emptyRegistry();
|
|
362
|
+
hasWarnedNoLayer = false;
|
|
363
|
+
}
|
|
364
|
+
let hasWarnedNoLayer = false;
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* One warning per module lifetime, and none in production — the same mechanism
|
|
368
|
+
* as the optional-peer boundaries. A missing `<MediaFlightLayer>` is otherwise
|
|
369
|
+
* completely silent: every `flyTo` resolves on its timeout and the app simply
|
|
370
|
+
* never shows a transition, which looks like the feature not being wired rather
|
|
371
|
+
* than the root not being.
|
|
372
|
+
*/
|
|
373
|
+
function warnNoLayerMounted() {
|
|
374
|
+
if (process.env.NODE_ENV === 'production' || hasWarnedNoLayer) return;
|
|
375
|
+
hasWarnedNoLayer = true;
|
|
376
|
+
// eslint-disable-next-line no-console
|
|
377
|
+
console.warn('[Bloom] flyTo() timed out waiting for a media surface to mount. Nothing is ' + 'rendering <MediaFlightLayer>, so no media transition will ever run and a ' + 'video opened from a feed will restart. Mount it ONCE at the app root ' + '(app/_layout.tsx), above the router.');
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/** Rect at `t` along `from` → `to`, so a retarget resumes rather than snapping. */
|
|
381
|
+
function interpolateRect(from, to, t) {
|
|
382
|
+
const clamped = t < 0 ? 0 : t > 1 ? 1 : t;
|
|
383
|
+
return {
|
|
384
|
+
x: from.x + (to.x - from.x) * clamped,
|
|
385
|
+
y: from.y + (to.y - from.y) * clamped,
|
|
386
|
+
width: from.width + (to.width - from.width) * clamped,
|
|
387
|
+
height: from.height + (to.height - from.height) * clamped
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Run one leg: 0 → 1 on the flight's own value, and on the shared `progress` a
|
|
393
|
+
* consumer reads.
|
|
394
|
+
*
|
|
395
|
+
* Both are driven with the SAME curve rather than one deriving from the other,
|
|
396
|
+
* because deriving would need a `useAnimatedReaction` — a hook, in a module with
|
|
397
|
+
* no component to hang it on. Two animations on two shared values cost a few
|
|
398
|
+
* bytes on the UI thread and keep the driver at the call site.
|
|
399
|
+
*
|
|
400
|
+
* The springs and the web timings are the media gallery's, imported rather than
|
|
401
|
+
* re-tuned: a flight that lands in the gallery and the gallery's own open
|
|
402
|
+
* transition are the same motion, and two copies of a spring config drift.
|
|
403
|
+
*/
|
|
404
|
+
function animate(flight, landing) {
|
|
405
|
+
const reg = registry();
|
|
406
|
+
const id = flight.id;
|
|
407
|
+
// EVERY leg reports its settlement, because the release condition is now
|
|
408
|
+
// "settled AND the destination is live" and either can arrive first. A
|
|
409
|
+
// `finished === false` callback means the leg was interrupted by a retarget,
|
|
410
|
+
// which owns the surface from then on.
|
|
411
|
+
//
|
|
412
|
+
// A LANDING leg is the one case that releases on its own: it has flown back to
|
|
413
|
+
// its anchor and there is nothing left to cover.
|
|
414
|
+
const onSettled = finished => {
|
|
415
|
+
'worklet';
|
|
416
|
+
|
|
417
|
+
if (finished === false) return;
|
|
418
|
+
runOnJS(landing ? releaseFlight : notifySurfaceSettled)(id);
|
|
419
|
+
};
|
|
420
|
+
flight.progress.value = 0;
|
|
421
|
+
reg.progress.value = 0;
|
|
422
|
+
if (Platform.OS === 'web') {
|
|
423
|
+
const duration = landing ? CLOSE_DURATION_WEB : OPEN_DURATION_WEB;
|
|
424
|
+
const easing = landing ? Easing.in(Easing.cubic) : Easing.out(Easing.cubic);
|
|
425
|
+
flight.progress.value = withTiming(1, {
|
|
426
|
+
duration,
|
|
427
|
+
easing
|
|
428
|
+
}, onSettled);
|
|
429
|
+
reg.progress.value = withTiming(1, {
|
|
430
|
+
duration,
|
|
431
|
+
easing
|
|
432
|
+
});
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
const spring = landing ? CLOSE_SPRING : OPEN_SPRING;
|
|
436
|
+
flight.progress.value = withSpring(1, spring, onSettled);
|
|
437
|
+
reg.progress.value = withSpring(1, spring);
|
|
438
|
+
}
|
|
439
|
+
//# sourceMappingURL=store.js.map
|