@ericchen1990/pano-view 3.4.1 → 3.4.2

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/README.md CHANGED
@@ -581,10 +581,12 @@ export function VideoExample() {
581
581
  ```
582
582
 
583
583
  `muted` defaults to `true` so `autoPlay` has a chance under browser autoplay
584
- rules. `playsInline` also defaults to true and writes both `playsinline` and
585
- `webkit-playsinline`, keeping the default 360-video path inline on iPhone
586
- instead of entering the native player. Unmuted playback still needs a user
587
- gesture; `onPlaybackStateChange` receives `"blocked"` when `play()` is
584
+ rules. `playsInline` also defaults to true and writes `playsinline`,
585
+ `webkit-playsinline`, and the X5 `h5-page` attributes, so the video stays in
586
+ the page on iPhone Safari and in WeChat, QQ, and UC. Picture-in-picture and
587
+ remote playback stay disabled so the element can keep feeding the sphere
588
+ texture. The element is mounted inside the viewer at 1×1px. Unmuted playback
589
+ still needs a user gesture; `onPlaybackStateChange` receives `"blocked"` when `play()` is
588
590
  rejected. Remote videos and VTT files that are not same-origin should set
589
591
  `crossOrigin` (usually `"anonymous"`). For mobile delivery, provide an H.264 +
590
592
  AAC MP4 fallback, poster, CORS, and Range support.
@@ -1641,7 +1643,9 @@ handler, `onEnded`, or playback error.
1641
1643
  `VideoHotspot` uses an `HTMLVideoElement` and `VideoTexture`. Its `playing`
1642
1644
  prop is likewise controlled: toggle it from your click handler or application
1643
1645
  state. On source change or unmount, the element is paused and its texture is
1644
- released. `playsInline` defaults to `true`, `muted` to `true`, `loop` to
1646
+ released. `playsInline` defaults to `true` and uses the same inline-playback
1647
+ attributes as `PanoVideo`. The element is mounted inside the viewer, hidden,
1648
+ so mobile browsers keep it in the page. `muted` defaults to `true`, `loop` to
1645
1649
  `false`, and `preload` to `"metadata"`.
1646
1650
 
1647
1651
  ```tsx
package/README.zh-CN.md CHANGED
@@ -538,7 +538,7 @@ export function VideoExample() {
538
538
  }
539
539
  ```
540
540
 
541
- `muted` 默认为 `true`,以便在浏览器自动播放规则下有机会自动播放;`playsInline` 也默认开启,并同时写入 `playsinline` / `webkit-playsinline`,避免 iPhone 将默认的 360 视频提取为原生全屏播放器。非静音播放仍需要用户手势;`play()` 被拒绝时 `onPlaybackStateChange` 会收到 `"blocked"`。非同源的远程视频与 VTT 文件应设置 `crossOrigin`(通常为 `"anonymous"`)。移动端请提供 H.264 + AAC 的 MP4 备选源、poster、CORS 与 Range 支持。
541
+ `muted` 默认为 `true`,以便在浏览器自动播放规则下有机会自动播放。`playsInline` 也默认开启,并写入 `playsinline`、`webkit-playsinline` 以及 X5 的 `h5-page` 属性,让视频在 iPhone Safari、微信、QQ 和 UC 中留在页面内。画中画和远程播放保持关闭,以便该元素持续作为球面纹理。元素会以 1×1 像素挂在查看器内。非静音播放仍需要用户手势;`play()` 被拒绝时 `onPlaybackStateChange` 会收到 `"blocked"`。非同源的远程视频与 VTT 文件应设置 `crossOrigin`(通常为 `"anonymous"`)。移动端请提供 H.264 + AAC 的 MP4 备选源、poster、CORS 与 Range 支持。
542
542
 
543
543
  ## Tile
544
544
 
@@ -1374,7 +1374,7 @@ import { SequenceHotspot } from "@ericchen1990/pano-view";
1374
1374
 
1375
1375
  ## VideoHotspot
1376
1376
 
1377
- `VideoHotspot` 使用 `HTMLVideoElement` 与 `VideoTexture`。其 `playing` prop 同样受控:在点击处理或应用状态中切换。源变更或卸载时元素暂停并释放纹理。`playsInline` 默认 `true`,`muted` 默认 `true`,`loop` 默认 `false`,`preload` 默认 `"metadata"`。
1377
+ `VideoHotspot` 使用 `HTMLVideoElement` 与 `VideoTexture`。其 `playing` prop 同样受控:在点击处理或应用状态中切换。源变更或卸载时元素暂停并释放纹理。`playsInline` 默认 `true`,并使用与 `PanoVideo` 相同的内联播放属性。元素会隐藏挂在查看器内,以便移动端浏览器把它留在页面中。`muted` 默认 `true`,`loop` 默认 `false`,`preload` 默认 `"metadata"`。
1378
1378
 
1379
1379
  ```tsx
1380
1380
  import { VideoHotspot } from "@ericchen1990/pano-view";
package/dist/index.cjs CHANGED
@@ -9096,6 +9096,56 @@ function useWebVRPointerLock(enabled, element) {
9096
9096
  // src/webvr/wakelock.ts
9097
9097
  var import_react33 = require("react");
9098
9098
 
9099
+ // src/video/inline-playback.ts
9100
+ var INLINE_VIDEO_ATTRIBUTES = [
9101
+ ["playsinline", "true"],
9102
+ ["webkit-playsinline", "true"],
9103
+ ["x5-playsinline", "true"],
9104
+ ["x5-video-player-type", "h5-page"],
9105
+ ["x5-video-player-fullscreen", "false"]
9106
+ ];
9107
+ function applyInlineVideoPlayback(video, inline) {
9108
+ video.controls = false;
9109
+ video.playsInline = inline;
9110
+ video.disablePictureInPicture = true;
9111
+ video.disableRemotePlayback = true;
9112
+ video.setAttribute("disablepictureinpicture", "");
9113
+ video.setAttribute("disableremoteplayback", "");
9114
+ video.setAttribute("x-webkit-airplay", "deny");
9115
+ video.setAttribute(
9116
+ "controlsList",
9117
+ "nodownload nofullscreen noremoteplayback"
9118
+ );
9119
+ if (inline) {
9120
+ for (const [name, value] of INLINE_VIDEO_ATTRIBUTES) {
9121
+ video.setAttribute(name, value);
9122
+ }
9123
+ return;
9124
+ }
9125
+ for (const [name] of INLINE_VIDEO_ATTRIBUTES) {
9126
+ video.removeAttribute(name);
9127
+ }
9128
+ }
9129
+ function mountTextureVideo(video, canvas) {
9130
+ const parent = canvas.closest("[data-pano-viewer]") ?? canvas.parentElement;
9131
+ if (!parent) {
9132
+ return;
9133
+ }
9134
+ video.tabIndex = -1;
9135
+ video.setAttribute("aria-hidden", "true");
9136
+ video.setAttribute("data-pano-texture-video", "");
9137
+ video.style.position = "absolute";
9138
+ video.style.top = "0";
9139
+ video.style.left = "0";
9140
+ video.style.width = "1px";
9141
+ video.style.height = "1px";
9142
+ video.style.opacity = "0";
9143
+ video.style.pointerEvents = "none";
9144
+ if (video.parentElement !== parent) {
9145
+ parent.appendChild(video);
9146
+ }
9147
+ }
9148
+
9099
9149
  // src/webvr/wakelock-fallback.ts
9100
9150
  var WEBVR_WAKELOCK_FALLBACK_VIDEO_BASE64 = "AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAbBbW9vdgAAAGxtdmhkAAAAAAAAAAAAAAAAAAAD6AAAD4AAAQAAAQAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAwAAAxt0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAABAAAAAAAAAggAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAABAAAAAQAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAAIIAAAEAAABAAAAAAKTbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAAyAAAAGgBVxAAAAAAALWhkbHIAAAAAAAAAAHZpZGUAAAAAAAAAAAAAAABWaWRlb0hhbmRsZXIAAAACPm1pbmYAAAAUdm1oZAAAAAEAAAAAAAAAAAAAACRkaW5mAAAAHGRyZWYAAAAAAAAAAQAAAAx1cmwgAAAAAQAAAf5zdGJsAAAAvnN0c2QAAAAAAAAAAQAAAK5hdmMxAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAABAAEABIAAAASAAAAAAAAAABFUxhdmM2Mi4xMS4xMDAgbGlieDI2NAAAAAAAAAAAAAAAGP//AAAANGF2Y0MBZAAK/+EAF2dkAAqs2V7ARAAAAwAEAAADAMg8SJZYAQAGaOvjyyLA/fj4AAAAABBwYXNwAAAAAQAAAAEAAAAUYnRydAAAAAAAADQ5AAAAAAAAABhzdHRzAAAAAAAAAAEAAAANAAACAAAAABRzdHNzAAAAAAAAAAEAAAABAAAAeGN0dHMAAAAAAAAADQAAAAEAAAQAAAAAAQAACgAAAAABAAAEAAAAAAEAAAAAAAAAAQAAAgAAAAABAAAKAAAAAAEAAAQAAAAAAQAAAAAAAAABAAACAAAAAAEAAAoAAAAAAQAABAAAAAABAAAAAAAAAAEAAAIAAAAAKHN0c2MAAAAAAAAAAgAAAAEAAAADAAAAAQAAAAUAAAABAAAAAQAAAEhzdHN6AAAAAAAAAAAAAAANAAACxQAAAAwAAAAMAAAADAAAAAwAAAASAAAADgAAAAwAAAAMAAAAEgAAAA4AAAAMAAAADAAAACRzdGNvAAAAAAAAAAUAAAcGAAAJ5wAAChUAAAo/AAAKbwAAAtF0cmFrAAAAXHRraGQAAAADAAAAAAAAAAAAAAACAAAAAAAAD4AAAAAAAAAAAAAAAAEBAAAAAAEAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAkZWR0cwAAABxlbHN0AAAAAAAAAAEAAA+AAAAEAAABAAAAAAJJbWRpYQAAACBtZGhkAAAAAAAAAAAAAAAAAAAfQAAAgABVxAAAAAAALWhkbHIAAAAAAAAAAHNvdW4AAAAAAAAAAAAAAABTb3VuZEhhbmRsZXIAAAAB9G1pbmYAAAAQc21oZAAAAAAAAAAAAAAAJGRpbmYAAAAcZHJlZgAAAAAAAAABAAAADHVybCAAAAABAAABuHN0YmwAAAB+c3RzZAAAAAAAAAABAAAAbm1wNGEAAAAAAAAAAQAAAAAAAAAAAAEAEAAAAAAfQAAAAAAANmVzZHMAAAAAA4CAgCUAAgAEgICAF0AVAAAAAAAfQAAAARsFgICABRWIVuUABoCAgAECAAAAFGJ0cnQAAAAAAAAfQAAAARsAAAAYc3R0cwAAAAAAAAABAAAAIAAABAAAAAAoc3RzYwAAAAAAAAACAAAAAQAAAAEAAAABAAAABgAAABsAAAABAAAAlHN0c3oAAAAAAAAAAAAAACAAAAAVAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAAQAAAAEAAAABAAAAChzdGNvAAAAAAAAAAYAAAbxAAAJ4wAAChEAAAo7AAAKawAACnsAAAAac2dwZAEAAAByb2xsAAAAAgAAAAH//wAAABxzYmdwAAAAAHJvbGwAAAABAAAAIAAAAAEAAABhdWR0YQAAAFltZXRhAAAAAAAAACFoZGxyAAAAAAAAAABtZGlyYXBwbAAAAAAAAAAAAAAAACxpbHN0AAAAJKl0b28AAAAcZGF0YQAAAAEAAAAATGF2ZjYyLjMuMTAwAAAACGZyZWUAAAP+bWRhdN4CAExhdmM2Mi4xMS4xMDAAAjBADgAAAq4GBf//qtxF6b3m2Ui3lizYINkj7u94MjY0IC0gY29yZSAxNjUgcjMyMjIgYjM1NjA1YSAtIEguMjY0L01QRUctNCBBVkMgY29kZWMgLSBDb3B5bGVmdCAyMDAzLTIwMjUgLSBodHRwOi8vd3d3LnZpZGVvbGFuLm9yZy94MjY0Lmh0bWwgLSBvcHRpb25zOiBjYWJhYz0xIHJlZj0zIGRlYmxvY2s9MTowOjAgYW5hbHlzZT0weDM6MHgxMTMgbWU9aGV4IHN1Ym1lPTcgcHN5PTEgcHN5X3JkPTEuMDA6MC4wMCBtaXhlZF9yZWY9MSBtZV9yYW5nZT0xNiBjaHJvbWFfbWU9MSB0cmVsbGlzPTEgOHg4ZGN0PTEgY3FtPTAgZGVhZHpvbmU9MjEsMTEgZmFzdF9wc2tpcD0xIGNocm9tYV9xcF9vZmZzZXQ9LTIgdGhyZWFkcz0xIGxvb2thaGVhZF90aHJlYWRzPTEgc2xpY2VkX3RocmVhZHM9MCBucj0wIGRlY2ltYXRlPTEgaW50ZXJsYWNlZD0wIGJsdXJheV9jb21wYXQ9MCBjb25zdHJhaW5lZF9pbnRyYT0wIGJmcmFtZXM9MyBiX3B5cmFtaWQ9MiBiX2FkYXB0PTEgYl9iaWFzPTAgZGlyZWN0PTEgd2VpZ2h0Yj0xIG9wZW5fZ29wPTAgd2VpZ2h0cD0yIGtleWludD0yNTAga2V5aW50X21pbj0yNSBzY2VuZWN1dD00MCBpbnRyYV9yZWZyZXNoPTAgcmNfbG9va2FoZWFkPTQwIHJjPWNyZiBtYnRyZWU9MSBjcmY9MjMuMCBxY29tcD0wLjYwIHFwbWluPTAgcXBtYXg9NjkgcXBzdGVwPTQgaXBfcmF0aW89MS40MCBhcT0xOjEuMDAAgAAAAA9liIQAO//+906/AptUwmEAAAAIQZokbEN//uAAAAAIQZ5CeIX/wYEBGCAHAAAACAGeYXRCv8SAAAAACAGeY2pCv8SBAAAADkGaaEmoQWiZTAhn//7hARggBwAAAApBnoZFESwv/8GBAAAACAGepXRCv8SBAAAACAGep2pCv8SAARggBwAAAA5BmqxJqEFsmUwIV//+wAAAAApBnspFFSwv/8GBAAAACAGe6XRCv8SAARggBwAAAAgBnutqQr/EgAEYIAcBGCAHARggBwEYIAcBGCAHARggBwEYIAcBGCAHARggBwEYIAcBGCAHARggBwEYIAcBGCAHARggBwEYIAcBGCAHARggBwEYIAcBGCAHARggBwEYIAcBGCAHARggBwEYIAcBGCAHARggBw==";
9101
9151
 
@@ -9121,17 +9171,12 @@ function decodeBase64(value) {
9121
9171
  function createFallbackVideo() {
9122
9172
  const video = document.createElement("video");
9123
9173
  video.autoplay = true;
9124
- video.controls = false;
9125
9174
  video.defaultMuted = true;
9126
9175
  video.loop = true;
9127
9176
  video.muted = true;
9128
- video.playsInline = true;
9129
9177
  video.tabIndex = -1;
9130
9178
  video.ariaHidden = "true";
9131
- video.disablePictureInPicture = true;
9132
- video.setAttribute("playsinline", "");
9133
- video.setAttribute("webkit-playsinline", "");
9134
- video.setAttribute("disablepictureinpicture", "");
9179
+ applyInlineVideoPlayback(video, true);
9135
9180
  video.style.cssText = "pointer-events:none;position:fixed;bottom:0;left:0;width:1px;height:1px;opacity:0;";
9136
9181
  const src = URL.createObjectURL(
9137
9182
  new Blob([decodeBase64(WEBVR_WAKELOCK_FALLBACK_VIDEO_BASE64)], {
@@ -11066,7 +11111,6 @@ function PanoVideo({
11066
11111
  usePanoMediaActivationIntent(autoPlay);
11067
11112
  const gl = (0, import_fiber18.useThree)((state) => state.gl);
11068
11113
  const host = (0, import_react42.useContext)(PanoVideoHostContext);
11069
- const overlay = (0, import_react42.useContext)(PanoChromeOverlayContext);
11070
11114
  const variants = (0, import_react42.useMemo)(
11071
11115
  () => resolvePanoVideoVariants({ poster, src, variants: variantsProp }),
11072
11116
  [poster, src, variantsProp]
@@ -11144,9 +11188,7 @@ function PanoVideo({
11144
11188
  }
11145
11189
  const video = document.createElement("video");
11146
11190
  video.muted = true;
11147
- video.playsInline = true;
11148
- video.setAttribute("playsinline", "true");
11149
- video.setAttribute("webkit-playsinline", "true");
11191
+ applyInlineVideoPlayback(video, true);
11150
11192
  video.preload = "metadata";
11151
11193
  const texture = new import_three16.VideoTexture(video);
11152
11194
  applyEquirectangularLayout(texture, 1);
@@ -11176,22 +11218,15 @@ function PanoVideo({
11176
11218
  }
11177
11219
  resource.texture.anisotropy = gl.capabilities.getMaxAnisotropy();
11178
11220
  }, [gl, resource]);
11179
- (0, import_react42.useEffect)(() => {
11180
- if (!resource || !overlay?.overlayElement) {
11221
+ (0, import_react42.useLayoutEffect)(() => {
11222
+ if (!resource) {
11181
11223
  return;
11182
11224
  }
11183
- const { video } = resource;
11184
- video.setAttribute("data-pano-video", "");
11185
- video.style.position = "absolute";
11186
- video.style.width = "1px";
11187
- video.style.height = "1px";
11188
- video.style.opacity = "0";
11189
- video.style.pointerEvents = "none";
11190
- overlay.overlayElement.appendChild(video);
11225
+ mountTextureVideo(resource.video, gl.domElement);
11191
11226
  return () => {
11192
- video.remove();
11227
+ resource.video.remove();
11193
11228
  };
11194
- }, [overlay?.overlayElement, resource]);
11229
+ }, [gl, resource]);
11195
11230
  const resolvedVariantId = resolvePanoVideoVariantId(variants, variantId);
11196
11231
  const activeVariant = variants.find((variant) => variant.id === resolvedVariantId) ?? variants[0];
11197
11232
  const mediaKey = activeVariant ? `${activeVariant.id}:${activeVariant.sources.map((source) => source.src).join("|")}:${tracks.map((track) => `${panoVideoTrackId(track)}:${track.src}`).join("|")}:${crossOrigin ?? ""}` : "";
@@ -11481,7 +11516,7 @@ function PanoVideo({
11481
11516
  }
11482
11517
  };
11483
11518
  }, [emitSnapshot, resource]);
11484
- (0, import_react42.useEffect)(() => {
11519
+ (0, import_react42.useLayoutEffect)(() => {
11485
11520
  if (!resource) {
11486
11521
  return;
11487
11522
  }
@@ -11491,14 +11526,7 @@ function PanoVideo({
11491
11526
  if (mediaCapabilitiesRef.current.volumeAdjustable) {
11492
11527
  video.volume = clampVolume(volume);
11493
11528
  }
11494
- video.playsInline = playsInline;
11495
- if (playsInline) {
11496
- video.setAttribute("playsinline", "true");
11497
- video.setAttribute("webkit-playsinline", "true");
11498
- } else {
11499
- video.removeAttribute("playsinline");
11500
- video.removeAttribute("webkit-playsinline");
11501
- }
11529
+ applyInlineVideoPlayback(video, playsInline);
11502
11530
  video.preload = preload;
11503
11531
  }, [loop, muted, playsInline, preload, resource, volume]);
11504
11532
  (0, import_react42.useEffect)(() => {
@@ -15865,6 +15893,7 @@ function useVideoResource({
15865
15893
  poster,
15866
15894
  src
15867
15895
  }) {
15896
+ const gl = (0, import_fiber29.useThree)((state) => state.gl);
15868
15897
  const [resource, setResource] = (0, import_react57.useState)(null);
15869
15898
  const onEndedRef = (0, import_react57.useRef)(onEnded);
15870
15899
  const onErrorRef = (0, import_react57.useRef)(onError);
@@ -15882,12 +15911,7 @@ function useVideoResource({
15882
15911
  if (crossOrigin !== void 0) {
15883
15912
  video.crossOrigin = crossOrigin;
15884
15913
  }
15885
- const inline = playsInline ?? true;
15886
- video.playsInline = inline;
15887
- if (inline) {
15888
- video.setAttribute("playsinline", "true");
15889
- video.setAttribute("webkit-playsinline", "true");
15890
- }
15914
+ applyInlineVideoPlayback(video, playsInline ?? true);
15891
15915
  video.poster = poster ?? "";
15892
15916
  const texture = new import_three35.VideoTexture(video);
15893
15917
  texture.colorSpace = import_three35.SRGBColorSpace;
@@ -15929,6 +15953,7 @@ function useVideoResource({
15929
15953
  video.addEventListener("error", reportError);
15930
15954
  video.addEventListener("pause", handlePause);
15931
15955
  video.addEventListener("play", handlePlay);
15956
+ mountTextureVideo(video, gl.domElement);
15932
15957
  setResource({ video, texture, ready: false });
15933
15958
  video.src = src;
15934
15959
  video.load();
@@ -15942,9 +15967,19 @@ function useVideoResource({
15942
15967
  video.pause();
15943
15968
  video.removeAttribute("src");
15944
15969
  video.load();
15970
+ video.remove();
15945
15971
  texture.dispose();
15946
15972
  };
15947
- }, [crossOrigin, id, playsInline, poster, src]);
15973
+ }, [crossOrigin, gl, id, playsInline, poster, src]);
15974
+ (0, import_react57.useLayoutEffect)(() => {
15975
+ if (!resource) {
15976
+ return;
15977
+ }
15978
+ mountTextureVideo(resource.video, gl.domElement);
15979
+ return () => {
15980
+ resource.video.remove();
15981
+ };
15982
+ }, [gl, resource]);
15948
15983
  return resource;
15949
15984
  }
15950
15985
  function VideoHotspot({
@@ -15983,21 +16018,14 @@ function VideoHotspot({
15983
16018
  const onPlaybackStateChangeRef = (0, import_react57.useRef)(onPlaybackStateChange);
15984
16019
  onPlaybackErrorRef.current = onPlaybackError;
15985
16020
  onPlaybackStateChangeRef.current = onPlaybackStateChange;
15986
- (0, import_react57.useEffect)(() => {
16021
+ (0, import_react57.useLayoutEffect)(() => {
15987
16022
  if (!resource) {
15988
16023
  return;
15989
16024
  }
15990
16025
  resource.video.loop = loop;
15991
16026
  resource.video.muted = muted;
15992
16027
  resource.video.volume = clampVolume2(volume);
15993
- resource.video.playsInline = playsInline;
15994
- if (playsInline) {
15995
- resource.video.setAttribute("playsinline", "true");
15996
- resource.video.setAttribute("webkit-playsinline", "true");
15997
- } else {
15998
- resource.video.removeAttribute("playsinline");
15999
- resource.video.removeAttribute("webkit-playsinline");
16000
- }
16028
+ applyInlineVideoPlayback(resource.video, playsInline);
16001
16029
  resource.video.preload = preload;
16002
16030
  }, [loop, muted, playsInline, preload, resource, volume]);
16003
16031
  (0, import_react57.useEffect)(() => {