@flamingo-stack/openframe-frontend-core 0.0.551 → 0.0.552-snapshot.20260817220121
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/dist/{chunk-2YLMNXLB.js → chunk-25BXB4SH.js} +4 -4
- package/dist/{chunk-TXFJFO4A.js → chunk-3OBWO6N6.js} +2 -2
- package/dist/{chunk-NRTKEPUX.cjs → chunk-6WA3RI55.cjs} +18 -18
- package/dist/{chunk-NRTKEPUX.cjs.map → chunk-6WA3RI55.cjs.map} +1 -1
- package/dist/{chunk-MEY3PYOL.cjs → chunk-B5N2IG4Z.cjs} +7 -7
- package/dist/{chunk-MEY3PYOL.cjs.map → chunk-B5N2IG4Z.cjs.map} +1 -1
- package/dist/{chunk-EZIKNMYV.js → chunk-B7TI3RYG.js} +2 -2
- package/dist/{chunk-SWDTM5O6.js → chunk-BUNDMGAD.js} +7 -7
- package/dist/{chunk-7OPWSWEJ.cjs → chunk-CEAAOVCV.cjs} +1 -1
- package/dist/{chunk-7OPWSWEJ.cjs.map → chunk-CEAAOVCV.cjs.map} +1 -1
- package/dist/{chunk-H4RHXJHF.js → chunk-CGIZ4UGO.js} +2 -2
- package/dist/{chunk-YA5JDHWE.cjs → chunk-EGPI3SFI.cjs} +101 -101
- package/dist/{chunk-YA5JDHWE.cjs.map → chunk-EGPI3SFI.cjs.map} +1 -1
- package/dist/{chunk-O24VSDVN.cjs → chunk-LL6EPMAD.cjs} +42 -42
- package/dist/{chunk-O24VSDVN.cjs.map → chunk-LL6EPMAD.cjs.map} +1 -1
- package/dist/{chunk-YEXTFNVE.js → chunk-LZGHXN2E.js} +10 -10
- package/dist/chunk-LZGHXN2E.js.map +1 -0
- package/dist/{chunk-EFNDA2CG.js → chunk-MSNTPZJ3.js} +3 -3
- package/dist/{chunk-NVK7YZHN.js → chunk-N22NG622.js} +2 -2
- package/dist/{chunk-V3HNIPTE.cjs → chunk-NFQEDT43.cjs} +23 -23
- package/dist/{chunk-V3HNIPTE.cjs.map → chunk-NFQEDT43.cjs.map} +1 -1
- package/dist/{chunk-AK4LW6PF.cjs → chunk-OI56TF4Y.cjs} +42 -42
- package/dist/{chunk-AK4LW6PF.cjs.map → chunk-OI56TF4Y.cjs.map} +1 -1
- package/dist/{chunk-LQPHB2YF.js → chunk-OR67WC32.js} +2 -2
- package/dist/{chunk-H4OIDXP4.cjs → chunk-PWWMA3EJ.cjs} +36 -36
- package/dist/{chunk-H4OIDXP4.cjs.map → chunk-PWWMA3EJ.cjs.map} +1 -1
- package/dist/{chunk-4QYUNDS3.cjs → chunk-Q6RC3YGW.cjs} +3 -3
- package/dist/{chunk-4QYUNDS3.cjs.map → chunk-Q6RC3YGW.cjs.map} +1 -1
- package/dist/{chunk-IFH47QXW.js → chunk-QOS27D5S.js} +3 -3
- package/dist/{chunk-5RJUHUCI.cjs → chunk-QTO3WYB2.cjs} +5 -5
- package/dist/{chunk-5RJUHUCI.cjs.map → chunk-QTO3WYB2.cjs.map} +1 -1
- package/dist/{chunk-ZDWWXME5.cjs → chunk-S7ATADRE.cjs} +14 -14
- package/dist/{chunk-ZDWWXME5.cjs.map → chunk-S7ATADRE.cjs.map} +1 -1
- package/dist/{chunk-MKDL3S4U.cjs → chunk-T7UUDSDW.cjs} +3 -3
- package/dist/{chunk-MKDL3S4U.cjs.map → chunk-T7UUDSDW.cjs.map} +1 -1
- package/dist/{chunk-MVK7SNVO.js → chunk-UVAB3XAB.js} +6 -6
- package/dist/{chunk-Y2HKHGEW.js → chunk-UWY722RH.js} +1 -1
- package/dist/{chunk-Y2HKHGEW.js.map → chunk-UWY722RH.js.map} +1 -1
- package/dist/{chunk-ZYQAFJLF.cjs → chunk-V643QZFI.cjs} +4 -4
- package/dist/{chunk-ZYQAFJLF.cjs.map → chunk-V643QZFI.cjs.map} +1 -1
- package/dist/{chunk-4A736Z6S.js → chunk-VGLDA77S.js} +2 -2
- package/dist/{chunk-RJFD55SK.js → chunk-VYVOER73.js} +4 -4
- package/dist/{chunk-V5PDJFZS.js → chunk-WLQER7GC.js} +18 -13
- package/dist/chunk-WLQER7GC.js.map +1 -0
- package/dist/{chunk-T2JKOG6K.cjs → chunk-XBKBBKPR.cjs} +7 -7
- package/dist/{chunk-T2JKOG6K.cjs.map → chunk-XBKBBKPR.cjs.map} +1 -1
- package/dist/{chunk-PPPIBTCO.cjs → chunk-XPRFVYES.cjs} +14 -14
- package/dist/{chunk-PPPIBTCO.cjs.map → chunk-XPRFVYES.cjs.map} +1 -1
- package/dist/{chunk-72ZPQTPN.cjs → chunk-Y2WE2GOL.cjs} +96 -91
- package/dist/chunk-Y2WE2GOL.cjs.map +1 -0
- package/dist/{chunk-OMSEQDCU.js → chunk-YI2735PG.js} +3 -3
- package/dist/{chunk-CPADTPX4.cjs → chunk-ZGQZLUO6.cjs} +1035 -940
- package/dist/chunk-ZGQZLUO6.cjs.map +1 -0
- package/dist/{chunk-JI4JR3KC.js → chunk-ZS7VC5SX.js} +387 -292
- package/dist/chunk-ZS7VC5SX.js.map +1 -0
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/entity-cards/chat-video-entity-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/dispatch.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/captions-url.cjs +39 -1
- package/dist/components/features/captions-url.cjs.map +1 -1
- package/dist/components/features/captions-url.d.ts +59 -0
- package/dist/components/features/captions-url.d.ts.map +1 -1
- package/dist/components/features/captions-url.js +35 -2
- package/dist/components/features/captions-url.js.map +1 -1
- package/dist/components/features/entity-video-section.d.ts +12 -1
- package/dist/components/features/entity-video-section.d.ts.map +1 -1
- package/dist/components/features/index.cjs +16 -4
- package/dist/components/features/index.cjs.map +1 -1
- package/dist/components/features/index.d.ts +1 -0
- package/dist/components/features/index.d.ts.map +1 -1
- package/dist/components/features/index.js +15 -3
- package/dist/components/features/use-captions.d.ts +11 -0
- package/dist/components/features/use-captions.d.ts.map +1 -0
- package/dist/components/features/use-ios-native-video-fullscreen.d.ts +4 -0
- package/dist/components/features/use-ios-native-video-fullscreen.d.ts.map +1 -0
- package/dist/components/features/use-walkthrough-video.d.ts +4 -1
- package/dist/components/features/use-walkthrough-video.d.ts.map +1 -1
- package/dist/components/features/video.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +25 -25
- package/dist/components/help-center-pages/index.js +16 -16
- package/dist/components/index.cjs +153 -141
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +26 -14
- package/dist/components/index.js.map +1 -1
- package/dist/components/meeting-scheduler/index.cjs +35 -35
- package/dist/components/meeting-scheduler/index.js +5 -5
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/onboarding-guides/onboarding-guide-detail-view.d.ts.map +1 -1
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/shared/product-release/release-detail-page.d.ts.map +1 -1
- package/dist/components/tickets/index.cjs +8 -8
- package/dist/components/tickets/index.js +7 -7
- package/dist/components/ui/index.cjs +4 -4
- package/dist/components/ui/index.js +3 -3
- package/dist/contexts/chat-runtime-context.d.ts +8 -0
- package/dist/contexts/chat-runtime-context.d.ts.map +1 -1
- package/dist/contexts/index.cjs +2 -2
- package/dist/contexts/index.js +1 -1
- package/dist/hooks/index.cjs +3 -3
- package/dist/hooks/index.js +2 -2
- package/dist/index.cjs +16 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +15 -3
- package/package.json +1 -1
- package/src/components/chat/entity-cards/chat-video-entity-card.tsx +4 -0
- package/src/components/chat/entity-cards/dispatch.tsx +2 -0
- package/src/components/features/__tests__/use-ios-native-video-fullscreen.test.tsx +116 -0
- package/src/components/features/captions-url.ts +115 -1
- package/src/components/features/entity-video-section.tsx +22 -2
- package/src/components/features/index.ts +1 -0
- package/src/components/features/use-captions.ts +46 -0
- package/src/components/features/use-ios-native-video-fullscreen.ts +89 -0
- package/src/components/features/use-walkthrough-video.ts +17 -5
- package/src/components/features/video.tsx +5 -0
- package/src/components/onboarding-guides/onboarding-guide-detail-view.tsx +5 -3
- package/src/components/shared/product-release/release-detail-page.tsx +14 -4
- package/src/contexts/chat-runtime-context.tsx +8 -0
- package/dist/chunk-72ZPQTPN.cjs.map +0 -1
- package/dist/chunk-CPADTPX4.cjs.map +0 -1
- package/dist/chunk-JI4JR3KC.js.map +0 -1
- package/dist/chunk-V5PDJFZS.js.map +0 -1
- package/dist/chunk-YEXTFNVE.js.map +0 -1
- /package/dist/{chunk-2YLMNXLB.js.map → chunk-25BXB4SH.js.map} +0 -0
- /package/dist/{chunk-TXFJFO4A.js.map → chunk-3OBWO6N6.js.map} +0 -0
- /package/dist/{chunk-EZIKNMYV.js.map → chunk-B7TI3RYG.js.map} +0 -0
- /package/dist/{chunk-SWDTM5O6.js.map → chunk-BUNDMGAD.js.map} +0 -0
- /package/dist/{chunk-H4RHXJHF.js.map → chunk-CGIZ4UGO.js.map} +0 -0
- /package/dist/{chunk-EFNDA2CG.js.map → chunk-MSNTPZJ3.js.map} +0 -0
- /package/dist/{chunk-NVK7YZHN.js.map → chunk-N22NG622.js.map} +0 -0
- /package/dist/{chunk-LQPHB2YF.js.map → chunk-OR67WC32.js.map} +0 -0
- /package/dist/{chunk-IFH47QXW.js.map → chunk-QOS27D5S.js.map} +0 -0
- /package/dist/{chunk-MVK7SNVO.js.map → chunk-UVAB3XAB.js.map} +0 -0
- /package/dist/{chunk-4A736Z6S.js.map → chunk-VGLDA77S.js.map} +0 -0
- /package/dist/{chunk-RJFD55SK.js.map → chunk-VYVOER73.js.map} +0 -0
- /package/dist/{chunk-OMSEQDCU.js.map → chunk-YI2735PG.js.map} +0 -0
package/dist/index.js
CHANGED
|
@@ -702,6 +702,7 @@ import {
|
|
|
702
702
|
brandMarkDataUri,
|
|
703
703
|
buildAnchorProps,
|
|
704
704
|
buildAutoContinuationDirective,
|
|
705
|
+
buildCaptionsUrl,
|
|
705
706
|
buildChatAttachmentViewUrl,
|
|
706
707
|
buildDefaultHref,
|
|
707
708
|
buildDiscussAddendum,
|
|
@@ -717,6 +718,7 @@ import {
|
|
|
717
718
|
buildSuggestionUrl,
|
|
718
719
|
buildTicketOpenHref,
|
|
719
720
|
canonicalContentRefType,
|
|
721
|
+
captionsEntityTypeForDocType,
|
|
720
722
|
captureFirstTouchAttribution,
|
|
721
723
|
cardAwareUrlTransform,
|
|
722
724
|
changelogLabels,
|
|
@@ -848,6 +850,8 @@ import {
|
|
|
848
850
|
getDocAncestorNodeIds,
|
|
849
851
|
getDocSourceDefaultPath,
|
|
850
852
|
getDynamicIcon,
|
|
853
|
+
getEntityCaptionUrls,
|
|
854
|
+
getEntityCaptionUrlsById,
|
|
851
855
|
getExpandedRowModel,
|
|
852
856
|
getFacetedRowModel,
|
|
853
857
|
getFacetedUniqueValues,
|
|
@@ -954,6 +958,7 @@ import {
|
|
|
954
958
|
readDismissCookie,
|
|
955
959
|
readFetchedCardTitle,
|
|
956
960
|
readLeadingDecisionFrame,
|
|
961
|
+
rebaseCaptionsUrl,
|
|
957
962
|
rehypeStripUnsafe,
|
|
958
963
|
releaseStatusOptions,
|
|
959
964
|
releaseTypeOptions,
|
|
@@ -1012,6 +1017,7 @@ import {
|
|
|
1012
1017
|
urlPathLooksLikeSvg,
|
|
1013
1018
|
useAppLayoutDrawerContainer,
|
|
1014
1019
|
useBoardCollapse,
|
|
1020
|
+
useCaptions,
|
|
1015
1021
|
useChat,
|
|
1016
1022
|
useChatAttachmentImageGallery,
|
|
1017
1023
|
useChatAttachments,
|
|
@@ -1064,7 +1070,7 @@ import {
|
|
|
1064
1070
|
withFirstTouchAttribution,
|
|
1065
1071
|
withWalkthroughOpenParam,
|
|
1066
1072
|
writeDismissCookie
|
|
1067
|
-
} from "./chunk-
|
|
1073
|
+
} from "./chunk-ZS7VC5SX.js";
|
|
1068
1074
|
import "./chunk-7EUIW46X.js";
|
|
1069
1075
|
import "./chunk-LXC6P2EO.js";
|
|
1070
1076
|
import "./chunk-EL5YVPD5.js";
|
|
@@ -1200,7 +1206,7 @@ import {
|
|
|
1200
1206
|
validateSchema,
|
|
1201
1207
|
validateVariables,
|
|
1202
1208
|
variablesToUrlParams
|
|
1203
|
-
} from "./chunk-
|
|
1209
|
+
} from "./chunk-VGLDA77S.js";
|
|
1204
1210
|
import "./chunk-ZJJERZUA.js";
|
|
1205
1211
|
import {
|
|
1206
1212
|
AboutIcon,
|
|
@@ -1361,7 +1367,7 @@ import {
|
|
|
1361
1367
|
PLAY_ICON_PATH
|
|
1362
1368
|
} from "./chunk-JMZH4KMJ.js";
|
|
1363
1369
|
import "./chunk-W72U7OU7.js";
|
|
1364
|
-
import "./chunk-
|
|
1370
|
+
import "./chunk-UWY722RH.js";
|
|
1365
1371
|
import {
|
|
1366
1372
|
Checkbox,
|
|
1367
1373
|
DropdownMenu,
|
|
@@ -2307,6 +2313,7 @@ export {
|
|
|
2307
2313
|
breakpoints,
|
|
2308
2314
|
buildAnchorProps,
|
|
2309
2315
|
buildAutoContinuationDirective,
|
|
2316
|
+
buildCaptionsUrl,
|
|
2310
2317
|
buildChatAttachmentViewUrl,
|
|
2311
2318
|
buildDefaultHref,
|
|
2312
2319
|
buildDiscussAddendum,
|
|
@@ -2324,6 +2331,7 @@ export {
|
|
|
2324
2331
|
buildTicketOpenHref,
|
|
2325
2332
|
buttonVariants,
|
|
2326
2333
|
canonicalContentRefType,
|
|
2334
|
+
captionsEntityTypeForDocType,
|
|
2327
2335
|
captureFirstTouchAttribution,
|
|
2328
2336
|
cardAwareUrlTransform,
|
|
2329
2337
|
changelogLabels,
|
|
@@ -2483,6 +2491,8 @@ export {
|
|
|
2483
2491
|
getDocSourceDefaultPath,
|
|
2484
2492
|
getDynamicIcon,
|
|
2485
2493
|
getEmbedProxyAuth,
|
|
2494
|
+
getEntityCaptionUrls,
|
|
2495
|
+
getEntityCaptionUrlsById,
|
|
2486
2496
|
getExpandedRowModel,
|
|
2487
2497
|
getFacetedRowModel,
|
|
2488
2498
|
getFacetedUniqueValues,
|
|
@@ -2618,6 +2628,7 @@ export {
|
|
|
2618
2628
|
readDismissCookie,
|
|
2619
2629
|
readFetchedCardTitle,
|
|
2620
2630
|
readLeadingDecisionFrame,
|
|
2631
|
+
rebaseCaptionsUrl,
|
|
2621
2632
|
rehypeStripUnsafe,
|
|
2622
2633
|
releaseStatusOptions,
|
|
2623
2634
|
releaseTypeOptions,
|
|
@@ -2700,6 +2711,7 @@ export {
|
|
|
2700
2711
|
useAutoLimitTags,
|
|
2701
2712
|
useBatchImages,
|
|
2702
2713
|
useBoardCollapse,
|
|
2714
|
+
useCaptions,
|
|
2703
2715
|
useChat,
|
|
2704
2716
|
useChatAttachmentImageGallery,
|
|
2705
2717
|
useChatAttachments,
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import React from 'react'
|
|
4
4
|
import { EntityVideoSection } from '../../features/entity-video-section'
|
|
5
|
+
import { captionsEntityTypeForDocType } from '../../features/captions-url'
|
|
5
6
|
import type { ChatRef } from '../chat-ref.types'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -55,6 +56,9 @@ export function ChatVideoEntityCard({
|
|
|
55
56
|
highlightVideoUrl={highlightUrl}
|
|
56
57
|
highlightVideoThumbnail={highlightPoster}
|
|
57
58
|
mainVideoPoster={poster}
|
|
59
|
+
// Captions derive INSIDE the section from entity identity via the
|
|
60
|
+
// runtime's captions base — no caption data on the wire at all.
|
|
61
|
+
captionsEntity={{ type: captionsEntityTypeForDocType(chatRef.type), id: chatRef.id }}
|
|
58
62
|
title={chatRef.title}
|
|
59
63
|
// Intentionally omitted for chat density:
|
|
60
64
|
// videoSummary — assistant text above already covers it.
|
|
@@ -418,10 +418,12 @@ function itemVideoMetadata(item: any): Record<string, string> | null {
|
|
|
418
418
|
item?.highlight_video_thumbnail,
|
|
419
419
|
item?.featured_image,
|
|
420
420
|
)
|
|
421
|
+
const highlightVideoPoster = pick(m.highlightVideoPoster, item?.highlight_video_thumbnail)
|
|
421
422
|
if (videoUrl) out.videoUrl = videoUrl
|
|
422
423
|
if (youtubeUrl) out.youtubeUrl = youtubeUrl
|
|
423
424
|
if (highlightVideoUrl) out.highlightVideoUrl = highlightVideoUrl
|
|
424
425
|
if (videoPoster) out.videoPoster = videoPoster
|
|
426
|
+
if (highlightVideoPoster) out.highlightVideoPoster = highlightVideoPoster
|
|
425
427
|
return out.videoUrl || out.youtubeUrl || out.highlightVideoUrl ? out : null
|
|
426
428
|
}
|
|
427
429
|
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { renderHook } from '@testing-library/react';
|
|
4
|
+
import { useIosNativeVideoFullscreen } from '../use-ios-native-video-fullscreen';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The composed tree media-chrome dispatches through: the request starts at the
|
|
8
|
+
* fullscreen button inside `<media-controller>`, inside the player host's shadow
|
|
9
|
+
* root. The controller listener stands in for the handler that would service the
|
|
10
|
+
* request as element fullscreen when nothing claims it first.
|
|
11
|
+
*/
|
|
12
|
+
function mountPlayer({ supportsFullscreen = true }: { supportsFullscreen?: boolean } = {}) {
|
|
13
|
+
const host = document.createElement('div');
|
|
14
|
+
document.body.append(host);
|
|
15
|
+
const controller = document.createElement('div');
|
|
16
|
+
host.attachShadow({ mode: 'open' }).append(controller);
|
|
17
|
+
const button = document.createElement('button');
|
|
18
|
+
controller.append(button);
|
|
19
|
+
|
|
20
|
+
const webkitEnterFullscreen = vi.fn();
|
|
21
|
+
const video = Object.assign(document.createElement('video'), {
|
|
22
|
+
webkitSupportsFullscreen: supportsFullscreen,
|
|
23
|
+
webkitEnterFullscreen,
|
|
24
|
+
});
|
|
25
|
+
Object.assign(host, { media: { nativeEl: video } });
|
|
26
|
+
|
|
27
|
+
const serviced = vi.fn();
|
|
28
|
+
controller.addEventListener('mediaenterfullscreenrequest', serviced);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
playerRef: { current: host as unknown },
|
|
32
|
+
pressFullscreen: () =>
|
|
33
|
+
button.dispatchEvent(new CustomEvent('mediaenterfullscreenrequest', { bubbles: true, composed: true })),
|
|
34
|
+
serviced,
|
|
35
|
+
webkitEnterFullscreen,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function setShell(platform: 'ios' | 'android' | null): void {
|
|
40
|
+
if (platform === null) {
|
|
41
|
+
delete (globalThis as { Capacitor?: unknown }).Capacitor;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
(globalThis as { Capacitor?: unknown }).Capacitor = {
|
|
45
|
+
isNativePlatform: () => true,
|
|
46
|
+
getPlatform: () => platform,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
setShell(null);
|
|
52
|
+
document.body.innerHTML = '';
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
describe('useIosNativeVideoFullscreen', () => {
|
|
56
|
+
it('hands the request to the system player instead of the fullscreen API', () => {
|
|
57
|
+
setShell('ios');
|
|
58
|
+
const { playerRef, pressFullscreen, serviced, webkitEnterFullscreen } = mountPlayer();
|
|
59
|
+
renderHook(() => useIosNativeVideoFullscreen(playerRef));
|
|
60
|
+
|
|
61
|
+
pressFullscreen();
|
|
62
|
+
|
|
63
|
+
expect(webkitEnterFullscreen).toHaveBeenCalledTimes(1);
|
|
64
|
+
// Stopped in the capture phase, so element fullscreen is never requested.
|
|
65
|
+
expect(serviced).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('leaves the request to the player off the iOS shell', () => {
|
|
69
|
+
setShell(null);
|
|
70
|
+
const { playerRef, pressFullscreen, serviced, webkitEnterFullscreen } = mountPlayer();
|
|
71
|
+
renderHook(() => useIosNativeVideoFullscreen(playerRef));
|
|
72
|
+
|
|
73
|
+
pressFullscreen();
|
|
74
|
+
|
|
75
|
+
expect(webkitEnterFullscreen).not.toHaveBeenCalled();
|
|
76
|
+
expect(serviced).toHaveBeenCalledTimes(1);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('leaves the request to the player on the Android shell', () => {
|
|
80
|
+
setShell('android');
|
|
81
|
+
const { playerRef, pressFullscreen, serviced, webkitEnterFullscreen } = mountPlayer();
|
|
82
|
+
renderHook(() => useIosNativeVideoFullscreen(playerRef));
|
|
83
|
+
|
|
84
|
+
pressFullscreen();
|
|
85
|
+
|
|
86
|
+
expect(webkitEnterFullscreen).not.toHaveBeenCalled();
|
|
87
|
+
expect(serviced).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// `webkitSupportsFullscreen` stays false until metadata loads — reachable on a
|
|
91
|
+
// Save-Data connection, where preload is 'none'.
|
|
92
|
+
it('leaves the request to the player while the video reports no fullscreen support', () => {
|
|
93
|
+
setShell('ios');
|
|
94
|
+
const { playerRef, pressFullscreen, serviced, webkitEnterFullscreen } = mountPlayer({
|
|
95
|
+
supportsFullscreen: false,
|
|
96
|
+
});
|
|
97
|
+
renderHook(() => useIosNativeVideoFullscreen(playerRef));
|
|
98
|
+
|
|
99
|
+
pressFullscreen();
|
|
100
|
+
|
|
101
|
+
expect(webkitEnterFullscreen).not.toHaveBeenCalled();
|
|
102
|
+
expect(serviced).toHaveBeenCalledTimes(1);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('stops intercepting once unmounted', () => {
|
|
106
|
+
setShell('ios');
|
|
107
|
+
const { playerRef, pressFullscreen, serviced, webkitEnterFullscreen } = mountPlayer();
|
|
108
|
+
const { unmount } = renderHook(() => useIosNativeVideoFullscreen(playerRef));
|
|
109
|
+
|
|
110
|
+
unmount();
|
|
111
|
+
pressFullscreen();
|
|
112
|
+
|
|
113
|
+
expect(webkitEnterFullscreen).not.toHaveBeenCalled();
|
|
114
|
+
expect(serviced).toHaveBeenCalledTimes(1);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
@@ -30,5 +30,119 @@ export function getCaptionsUrl(
|
|
|
30
30
|
if (!srtContent) return undefined
|
|
31
31
|
const hash = `${srtContent.length}-${srtContent.slice(0, 8).replace(/\s/g, '')}`
|
|
32
32
|
const variant = options?.variant ? `&variant=${options.variant}` : ''
|
|
33
|
-
return
|
|
33
|
+
return `${DEFAULT_CAPTIONS_PATH}/${entityType}/${entityId}?v=${hash}${variant}`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The slice of `ChatRuntime.endpoints` this module needs. */
|
|
37
|
+
export interface CaptionsEndpoints {
|
|
38
|
+
/** Base URL prefix for the captions route (plain path base, no query
|
|
39
|
+
* params) — e.g. `/content/api/captions` in a proxied embedder. Unset ⇒
|
|
40
|
+
* the same-origin relative default `/api/captions` (the hub). Wired by
|
|
41
|
+
* hosts exactly like every other endpoint on `ChatRuntime.endpoints`. */
|
|
42
|
+
captionsUrlPrefix?: string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Same-origin default — for hosts that serve the route themselves (the hub).
|
|
46
|
+
* Cross-origin embedders set `endpoints.captionsUrlPrefix` instead. */
|
|
47
|
+
const DEFAULT_CAPTIONS_PATH = '/api/captions'
|
|
48
|
+
|
|
49
|
+
function resolveCaptionsBase(endpoints?: CaptionsEndpoints | null): string {
|
|
50
|
+
return endpoints?.captionsUrlPrefix || DEFAULT_CAPTIONS_PATH
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Rebase an already-built relative `/api/captions/...` URL (e.g. one the hub
|
|
54
|
+
* API computed server-side) onto the host's captions base. Same-origin hosts
|
|
55
|
+
* (base = default) get the URL back untouched. Non-captions URLs pass through. */
|
|
56
|
+
export function rebaseCaptionsUrl<T extends string | null | undefined>(
|
|
57
|
+
endpoints: CaptionsEndpoints | null | undefined,
|
|
58
|
+
url: T,
|
|
59
|
+
): T | string {
|
|
60
|
+
if (!url || !url.startsWith(`${DEFAULT_CAPTIONS_PATH}/`)) return url
|
|
61
|
+
const base = resolveCaptionsBase(endpoints)
|
|
62
|
+
if (base === DEFAULT_CAPTIONS_PATH) return url
|
|
63
|
+
return `${base}${url.slice(DEFAULT_CAPTIONS_PATH.length)}`
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The endpoints-aware entry point — `getCaptionsUrl` based on
|
|
68
|
+
* `endpoints.captionsUrlPrefix` (consumers hand over `runtime?.endpoints` and
|
|
69
|
+
* nothing else). Hub/same-origin hosts leave the prefix unset and resolve to
|
|
70
|
+
* the relative default.
|
|
71
|
+
*/
|
|
72
|
+
export function buildCaptionsUrl(
|
|
73
|
+
endpoints: CaptionsEndpoints | null | undefined,
|
|
74
|
+
entityType: string,
|
|
75
|
+
entityId: string | number,
|
|
76
|
+
srtContent?: string | null,
|
|
77
|
+
options?: { variant?: 'highlight' },
|
|
78
|
+
): string | undefined {
|
|
79
|
+
const rel = getCaptionsUrl(entityType, entityId, srtContent, options)
|
|
80
|
+
return rel ? (rebaseCaptionsUrl(endpoints, rel) as string) : undefined
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The two SRT columns every video entity carries — main video + highlight
|
|
84
|
+
* reel. The caption URL shape is fully generic over these
|
|
85
|
+
* (`?variant=highlight` is the only difference), so consumers never derive
|
|
86
|
+
* the two URLs by hand. */
|
|
87
|
+
export interface CaptionSrtFields {
|
|
88
|
+
id: string | number
|
|
89
|
+
srt_content?: string | null
|
|
90
|
+
highlight_srt_content?: string | null
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface EntityCaptionUrls {
|
|
94
|
+
captionsUrl?: string
|
|
95
|
+
highlightCaptionsUrl?: string
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* THE one-stop caption derivation for a video entity: hand over the runtime
|
|
100
|
+
* `endpoints` + the entity row and get both `<track>` URLs back, base-resolved
|
|
101
|
+
* for the host (relative on the hub, proxied in embedders). Every view
|
|
102
|
+
* (release, onboarding guide, webinar, podcast, …) calls this once instead of
|
|
103
|
+
* hand-building main + highlight URLs separately.
|
|
104
|
+
*/
|
|
105
|
+
export function getEntityCaptionUrls(
|
|
106
|
+
endpoints: CaptionsEndpoints | null | undefined,
|
|
107
|
+
entityType: string,
|
|
108
|
+
entity: CaptionSrtFields | null | undefined,
|
|
109
|
+
): EntityCaptionUrls {
|
|
110
|
+
if (!entity) return {}
|
|
111
|
+
return {
|
|
112
|
+
captionsUrl: buildCaptionsUrl(endpoints, entityType, entity.id, entity.srt_content),
|
|
113
|
+
highlightCaptionsUrl: buildCaptionsUrl(endpoints, entityType, entity.id, entity.highlight_srt_content, {
|
|
114
|
+
variant: 'highlight',
|
|
115
|
+
}),
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Identity-only variant of `getEntityCaptionUrls` — for surfaces that know the
|
|
121
|
+
* entity's type + id but NOT its SRT columns (chat cards, whose hydration rows
|
|
122
|
+
* deliberately exclude the heavy SRT content). The `/api/captions` route is
|
|
123
|
+
* addressed purely by `<entityType>/<entityId>`, so the URLs derive from
|
|
124
|
+
* identity alone; a video without subtitles costs one silent 404 `<track>`
|
|
125
|
+
* fetch. No `?v=` cache-buster (no content to hash) — the route's own
|
|
126
|
+
* Cache-Control governs freshness.
|
|
127
|
+
*/
|
|
128
|
+
export function getEntityCaptionUrlsById(
|
|
129
|
+
endpoints: CaptionsEndpoints | null | undefined,
|
|
130
|
+
entityType: string,
|
|
131
|
+
entityId: string | number,
|
|
132
|
+
): Required<EntityCaptionUrls> {
|
|
133
|
+
const base = resolveCaptionsBase(endpoints)
|
|
134
|
+
return {
|
|
135
|
+
captionsUrl: `${base}/${entityType}/${entityId}`,
|
|
136
|
+
highlightCaptionsUrl: `${base}/${entityType}/${entityId}?variant=highlight`,
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Chat docType → captions-route entity type. Chat documentTypes equal the
|
|
142
|
+
* captions entity types for every video-bearing type except podcast
|
|
143
|
+
* (`podcast` vs `podcast_episode`). Kept here (not per-card) so the alias
|
|
144
|
+
* exists exactly once.
|
|
145
|
+
*/
|
|
146
|
+
export function captionsEntityTypeForDocType(docType: string): string {
|
|
147
|
+
return docType === 'podcast' ? 'podcast_episode' : docType
|
|
34
148
|
}
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
} from '../ui/tabs';
|
|
10
10
|
import type { VideoTeaser } from '../../types/video-processing';
|
|
11
11
|
import { Video } from './video';
|
|
12
|
+
import { useCaptions } from './use-captions';
|
|
12
13
|
import { VideoBitesStrip, type VideoBiteStripItem } from './video-bites-strip';
|
|
13
14
|
import { DEFAULT_VIDEO_BITES_TITLE, type VideoBiteStripProfile } from './video-bites-shared';
|
|
14
15
|
import { SECTION_HEADING_CLASS } from '../layout/page-heading';
|
|
@@ -77,6 +78,14 @@ export interface EntityVideoSectionProps {
|
|
|
77
78
|
* player (`getCaptionsUrl(type, id, highlightSrt, { variant: 'highlight' })`).
|
|
78
79
|
*/
|
|
79
80
|
highlightCaptionsUrl?: string | null;
|
|
81
|
+
/**
|
|
82
|
+
* Entity identity for caption derivation — the alternative to the explicit
|
|
83
|
+
* URL props above for hosts that don't hold the entity's SRT columns (chat
|
|
84
|
+
* cards). The section derives both `<track>` URLs itself from the runtime's
|
|
85
|
+
* captions base (`getEntityCaptionUrlsById`); explicit `captionsUrl` /
|
|
86
|
+
* `highlightCaptionsUrl` win when also provided.
|
|
87
|
+
*/
|
|
88
|
+
captionsEntity?: { type: string; id: string | number } | null;
|
|
80
89
|
/** LCP hint — when true, the full-video tab's poster eager-loads. */
|
|
81
90
|
priority?: boolean;
|
|
82
91
|
}
|
|
@@ -99,14 +108,25 @@ export function EntityVideoSection({
|
|
|
99
108
|
bitesAutoScroll = true,
|
|
100
109
|
MarkdownRenderer,
|
|
101
110
|
srtContent,
|
|
102
|
-
captionsUrl,
|
|
103
|
-
highlightCaptionsUrl,
|
|
111
|
+
captionsUrl: captionsUrlProp,
|
|
112
|
+
highlightCaptionsUrl: highlightCaptionsUrlProp,
|
|
113
|
+
captionsEntity,
|
|
104
114
|
priority = false,
|
|
105
115
|
}: EntityVideoSectionProps) {
|
|
116
|
+
const captions = useCaptions();
|
|
106
117
|
const hasFullVideo = !!(youtubeUrl || mainVideoUrl);
|
|
107
118
|
const hasHighlight = !!highlightVideoUrl;
|
|
108
119
|
const hasVideo = hasFullVideo || hasHighlight;
|
|
109
120
|
|
|
121
|
+
// Caption resolution: explicit URL props win (detail pages gate on their
|
|
122
|
+
// entity's SRT columns); otherwise derive from `captionsEntity` identity —
|
|
123
|
+
// the zero-knowledge path for chat cards.
|
|
124
|
+
const derivedCaptions = captionsEntity
|
|
125
|
+
? captions.forEntityId(captionsEntity.type, captionsEntity.id)
|
|
126
|
+
: null;
|
|
127
|
+
const captionsUrl = captionsUrlProp ?? derivedCaptions?.captionsUrl;
|
|
128
|
+
const highlightCaptionsUrl = highlightCaptionsUrlProp ?? derivedCaptions?.highlightCaptionsUrl;
|
|
129
|
+
|
|
110
130
|
if (!hasVideo && !videoSummary && (!videoBites || videoBites.length === 0)) {
|
|
111
131
|
return null;
|
|
112
132
|
}
|
|
@@ -72,6 +72,7 @@ export * from './floating-walkthrough-video'
|
|
|
72
72
|
export * from './use-walkthrough-video'
|
|
73
73
|
export * from './use-video-warmup'
|
|
74
74
|
export * from './captions-url'
|
|
75
|
+
export * from './use-captions'
|
|
75
76
|
export * from './video-source-selector'
|
|
76
77
|
export * from './transcript-summary-editor'
|
|
77
78
|
export * from './subtitles-editor'
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* useCaptions — THE captions configurer.
|
|
5
|
+
*
|
|
6
|
+
* The ONE place in the lib that reads the chat-runtime endpoints
|
|
7
|
+
* (`endpoints.captionsUrlPrefix`) for caption purposes. Every caption
|
|
8
|
+
* consumer (`EntityVideoSection`, `ReleaseDetailPage`,
|
|
9
|
+
* `OnboardingGuideDetailView`, `useWalkthroughVideo`, …) calls this hook and
|
|
10
|
+
* uses the returned pre-bound helpers — no component reads runtime endpoint
|
|
11
|
+
* values for captions itself.
|
|
12
|
+
*
|
|
13
|
+
* The pure derivation logic lives in `captions-url.ts` (endpoint-agnostic,
|
|
14
|
+
* server-import-safe); this hook only binds it to the ambient runtime.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useMemo } from 'react'
|
|
18
|
+
import { useChatRuntime } from '../../contexts/chat-runtime-context'
|
|
19
|
+
import {
|
|
20
|
+
getEntityCaptionUrls,
|
|
21
|
+
getEntityCaptionUrlsById,
|
|
22
|
+
rebaseCaptionsUrl,
|
|
23
|
+
type CaptionSrtFields,
|
|
24
|
+
type EntityCaptionUrls,
|
|
25
|
+
} from './captions-url'
|
|
26
|
+
|
|
27
|
+
export interface CaptionsApi {
|
|
28
|
+
/** Both track URLs from an entity row's SRT columns (presence-gated). */
|
|
29
|
+
forEntity: (entityType: string, entity: CaptionSrtFields | null | undefined) => EntityCaptionUrls
|
|
30
|
+
/** Both track URLs from entity identity alone (no SRT knowledge — chat cards). */
|
|
31
|
+
forEntityId: (entityType: string, entityId: string | number) => Required<EntityCaptionUrls>
|
|
32
|
+
/** Rebase an already-built relative `/api/captions/...` URL onto the host base. */
|
|
33
|
+
rebase: <T extends string | null | undefined>(url: T) => T | string
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useCaptions(): CaptionsApi {
|
|
37
|
+
const endpoints = useChatRuntime()?.endpoints ?? null
|
|
38
|
+
return useMemo(
|
|
39
|
+
() => ({
|
|
40
|
+
forEntity: (entityType, entity) => getEntityCaptionUrls(endpoints, entityType, entity),
|
|
41
|
+
forEntityId: (entityType, entityId) => getEntityCaptionUrlsById(endpoints, entityType, entityId),
|
|
42
|
+
rebase: (url) => rebaseCaptionsUrl(endpoints, url),
|
|
43
|
+
}),
|
|
44
|
+
[endpoints],
|
|
45
|
+
)
|
|
46
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* iOS app shell only: hand the player's fullscreen control to Apple's own video
|
|
5
|
+
* player instead of the Fullscreen API.
|
|
6
|
+
*
|
|
7
|
+
* This is what media-chrome already does on iPhone Safari — `requestFullscreen`
|
|
8
|
+
* does not exist on `Element.prototype` there, so its `enterFullscreen` falls
|
|
9
|
+
* through to `media.webkitEnterFullscreen()` and the video is handed to the
|
|
10
|
+
* system player. The Capacitor shell turns element fullscreen ON (WKWebView's
|
|
11
|
+
* `isElementFullscreenEnabled`), so media-chrome takes the `requestFullscreen()`
|
|
12
|
+
* branch instead — and WebKit then hands the web view back with its scroll
|
|
13
|
+
* view's `contentInsetAdjustmentBehavior` reset from Capacitor's `.never` to
|
|
14
|
+
* `.automatic` and its own `safeAreaInsets` stranded at the fullscreen
|
|
15
|
+
* container's values. The app comes back from a video with a doubled top band
|
|
16
|
+
* and dead space above the home indicator until a rotation, and the shell can
|
|
17
|
+
* only paper over it afterwards (`MainViewController` KVOs `fullscreenState` to
|
|
18
|
+
* restore the behavior). Never entering element fullscreen is the actual fix.
|
|
19
|
+
*
|
|
20
|
+
* The takeover is an event interception, not a monkey-patch: the fullscreen
|
|
21
|
+
* button dispatches `mediaenterfullscreenrequest` (`composed`, `bubbles`) from
|
|
22
|
+
* inside the player's shadow trees, so a CAPTURE listener on the `<mux-player>`
|
|
23
|
+
* host sees it before the `<media-controller>` that would service it, and
|
|
24
|
+
* `stopImmediatePropagation()` is enough to claim the request. Which element
|
|
25
|
+
* media-chrome would have passed to `requestFullscreen` never enters into it.
|
|
26
|
+
*
|
|
27
|
+
* The player's own fullscreen STATE stays correct for free: the system player
|
|
28
|
+
* fires `webkitbeginfullscreen` / `webkitendfullscreen` on the `<video>`,
|
|
29
|
+
* `mux-video` re-dispatches both, and media-chrome's state mediator already
|
|
30
|
+
* listens for them.
|
|
31
|
+
*
|
|
32
|
+
* Anything the takeover cannot service falls through to media-chrome untouched:
|
|
33
|
+
* no `<video>` yet, or `webkitSupportsFullscreen` still false — it stays false
|
|
34
|
+
* until metadata has loaded, which is reachable on a Save-Data connection where
|
|
35
|
+
* preload is `'none'`.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import { useEffect } from 'react';
|
|
39
|
+
|
|
40
|
+
/** media-chrome's `MediaUIEvents.MEDIA_ENTER_FULLSCREEN_REQUEST`. Spelled out
|
|
41
|
+
* rather than imported: media-chrome is a transitive dep of the Mux player, not
|
|
42
|
+
* a declared one. */
|
|
43
|
+
const ENTER_FULLSCREEN_REQUEST = 'mediaenterfullscreenrequest';
|
|
44
|
+
|
|
45
|
+
/** The two `HTMLVideoElement` members WebKit adds for its own video-fullscreen
|
|
46
|
+
* presentation — absent from the DOM lib, and absent at runtime off Apple. */
|
|
47
|
+
interface NativeFullscreenVideo extends HTMLVideoElement {
|
|
48
|
+
webkitSupportsFullscreen?: boolean;
|
|
49
|
+
webkitEnterFullscreen?: () => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface CapacitorGlobal {
|
|
53
|
+
isNativePlatform?: () => boolean;
|
|
54
|
+
getPlatform?: () => string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Gated on the shell, not on the platform: element fullscreen behaves fine in
|
|
58
|
+
* Safari (iPad) and on the desktop shell, where taking it away would only cost
|
|
59
|
+
* the player its chrome in fullscreen. */
|
|
60
|
+
function isIosAppShell(): boolean {
|
|
61
|
+
const capacitor = (globalThis as { Capacitor?: CapacitorGlobal }).Capacitor;
|
|
62
|
+
return capacitor?.isNativePlatform?.() === true && capacitor.getPlatform?.() === 'ios';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The real `<video>` behind `<mux-player>`: `player.media` is the `<mux-video>`
|
|
66
|
+
* custom element and `nativeEl` is the element WebKit presents. */
|
|
67
|
+
function nativeVideoElement(player: EventTarget): NativeFullscreenVideo | null {
|
|
68
|
+
const media = (player as { media?: { nativeEl?: unknown } | null }).media;
|
|
69
|
+
const element = media?.nativeEl ?? media;
|
|
70
|
+
return element instanceof HTMLVideoElement ? element : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function useIosNativeVideoFullscreen(playerRef: { current: unknown }): void {
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
if (!isIosAppShell()) return;
|
|
76
|
+
const player = playerRef.current;
|
|
77
|
+
if (!(player instanceof EventTarget)) return;
|
|
78
|
+
|
|
79
|
+
const takeOver = (event: Event) => {
|
|
80
|
+
const video = nativeVideoElement(player);
|
|
81
|
+
if (!video?.webkitSupportsFullscreen || typeof video.webkitEnterFullscreen !== 'function') return;
|
|
82
|
+
event.stopImmediatePropagation();
|
|
83
|
+
video.webkitEnterFullscreen();
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
player.addEventListener(ENTER_FULLSCREEN_REQUEST, takeOver, true);
|
|
87
|
+
return () => player.removeEventListener(ENTER_FULLSCREEN_REQUEST, takeOver, true);
|
|
88
|
+
}, [playerRef]);
|
|
89
|
+
}
|
|
@@ -6,8 +6,11 @@
|
|
|
6
6
|
* SSOT for fetching + shaping the widget's data on the CLIENT (any embedder
|
|
7
7
|
* reuses it). The public endpoint returns the RAW body `{ walkthroughVideo }`
|
|
8
8
|
* (no {success,data} wrapper). The video's `captionsUrl` is a RELATIVE
|
|
9
|
-
* `/api/captions/...` path;
|
|
10
|
-
*
|
|
9
|
+
* `/api/captions/...` path; it is rebased automatically onto
|
|
10
|
+
* `ChatRuntime.endpoints.captionsUrlPrefix` (the standard runtime-endpoint
|
|
11
|
+
* wiring) — same-origin hosts leave that unset and get the URL unchanged.
|
|
12
|
+
* `transformCaptionsUrl` remains as an explicit override for hosts outside
|
|
13
|
+
* a runtime provider.
|
|
11
14
|
*
|
|
12
15
|
* SSR hosts (the hub) resolve the video server-side and pass it to the widget
|
|
13
16
|
* directly; they don't need this hook. It exists for client-only embedders.
|
|
@@ -15,6 +18,7 @@
|
|
|
15
18
|
|
|
16
19
|
import { useQuery } from '@tanstack/react-query';
|
|
17
20
|
import type { WalkthroughVideoData } from './floating-walkthrough-video';
|
|
21
|
+
import { useCaptions } from './use-captions';
|
|
18
22
|
|
|
19
23
|
export interface UseWalkthroughVideoOptions {
|
|
20
24
|
/** Absolute or proxied URL of the public GET route. */
|
|
@@ -22,7 +26,10 @@ export interface UseWalkthroughVideoOptions {
|
|
|
22
26
|
/** SSR/initial seed to avoid a loading flash. */
|
|
23
27
|
initialData?: WalkthroughVideoData | null;
|
|
24
28
|
enabled?: boolean;
|
|
25
|
-
/** Rewrite the RELATIVE captionsUrl (e.g. prefix a `/content` proxy base).
|
|
29
|
+
/** Rewrite the RELATIVE captionsUrl (e.g. prefix a `/content` proxy base).
|
|
30
|
+
* OPTIONAL override — when unset, the URL is rebased onto
|
|
31
|
+
* `ChatRuntime.endpoints.captionsUrlPrefix` (see `rebaseCaptionsUrl`), so
|
|
32
|
+
* embedders inside a runtime provider need no wiring here at all. */
|
|
26
33
|
transformCaptionsUrl?: (relativeUrl: string) => string;
|
|
27
34
|
}
|
|
28
35
|
|
|
@@ -33,6 +40,7 @@ export interface UseWalkthroughVideoResult {
|
|
|
33
40
|
|
|
34
41
|
export function useWalkthroughVideo(opts: UseWalkthroughVideoOptions): UseWalkthroughVideoResult {
|
|
35
42
|
const { endpoint, initialData, enabled = true, transformCaptionsUrl } = opts;
|
|
43
|
+
const captions = useCaptions();
|
|
36
44
|
|
|
37
45
|
const query = useQuery<WalkthroughVideoData | null>({
|
|
38
46
|
queryKey: ['walkthrough-video', endpoint],
|
|
@@ -59,10 +67,14 @@ export function useWalkthroughVideo(opts: UseWalkthroughVideoOptions): UseWalkth
|
|
|
59
67
|
// rewrite here (not in queryFn) means a second consumer of the same
|
|
60
68
|
// endpoint can't inherit this observer's proxied captionsUrl from the cache.
|
|
61
69
|
select: (video) => {
|
|
62
|
-
if (video?.captionsUrl
|
|
70
|
+
if (!video?.captionsUrl) return video;
|
|
71
|
+
if (transformCaptionsUrl && video.captionsUrl.startsWith('/')) {
|
|
63
72
|
return { ...video, captionsUrl: transformCaptionsUrl(video.captionsUrl) };
|
|
64
73
|
}
|
|
65
|
-
|
|
74
|
+
// Default: rebase onto the configured captions base. Same-origin hosts
|
|
75
|
+
// leave it unset and get the URL back unchanged — a no-op for them.
|
|
76
|
+
const rebased = captions.rebase(video.captionsUrl);
|
|
77
|
+
return rebased === video.captionsUrl ? video : { ...video, captionsUrl: rebased as string };
|
|
66
78
|
},
|
|
67
79
|
});
|
|
68
80
|
|
|
@@ -34,6 +34,7 @@ import React, { useCallback, useEffect, useImperativeHandle, useMemo, useRef, us
|
|
|
34
34
|
import MuxPlayer from '@mux/mux-player-react';
|
|
35
35
|
import { VideoPlayBadge, VideoUnmuteGlyph } from './video-center-badge';
|
|
36
36
|
import { fetchPriorityProp } from '../../utils/fetch-priority';
|
|
37
|
+
import { useIosNativeVideoFullscreen } from './use-ios-native-video-fullscreen';
|
|
37
38
|
import { saveDataEnabled } from './use-video-warmup';
|
|
38
39
|
|
|
39
40
|
// =============================================================================
|
|
@@ -938,6 +939,10 @@ function FilePlayer({
|
|
|
938
939
|
return () => { try { el.removeEventListener?.('ended', handler); } catch { /* ignore */ } };
|
|
939
940
|
}, []);
|
|
940
941
|
|
|
942
|
+
// In the iOS shell the fullscreen control goes to Apple's video player rather
|
|
943
|
+
// than to element fullscreen, which WebKit exits by breaking the safe areas.
|
|
944
|
+
useIosNativeVideoFullscreen(hoverPlayerRef);
|
|
945
|
+
|
|
941
946
|
// Imperative handle — snapshot getters + control mutators for handoff.
|
|
942
947
|
useImperativeHandle(playerHandleRef, (): VideoPlayerHandle => ({
|
|
943
948
|
getCurrentTime: () => hoverPlayerRef.current?.currentTime ?? 0,
|