1
0
Aiden ddbcebf80a
All checks were successful
Publish Pages / publish (push) Successful in 26s
Test / test (push) Successful in 13s
updated example video
2026-06-18 21:47:33 +10:00
2026-06-11 16:16:29 +10:00
2026-06-18 21:47:33 +10:00
2026-06-11 16:51:42 +10:00
2026-06-18 21:47:33 +10:00
2026-06-11 16:51:42 +10:00
2026-06-11 09:16:42 +10:00
2026-06-11 08:30:52 +10:00
2026-02-19 11:13:56 -06:00
2026-06-11 16:51:42 +10:00
2026-06-10 14:29:23 +10:00
2025-06-04 16:22:25 -05:00
2026-06-10 11:03:43 +10:00
2026-06-11 16:51:42 +10:00
2026-06-11 16:51:42 +10:00
2026-06-10 10:35:14 +10:00

VR Web Player

A CDN-friendly web player for side-by-side stereoscopic video and still images.

The player supports two projection modes:

  • vr180: an immersive 180 degree stereoscopic hemisphere in WebXR, with a draggable rectilinear fallback on non-XR browsers.
  • plane: a flat stereoscopic media plane in WebXR, with a normal flat left-eye fallback on non-XR browsers.

How to use it

Build the player first, then host the generated vr180player/ directory on your CDN and include the module script. The script automatically loads its matching CSS file from the same folder, and it imports its helper modules with relative module paths.

Current F40 Pages CDN entrypoint:

<script type="module" src="https://pages.f-40.com/VR-Web-Player/vr180player/vr180-player.js"></script>
<button
	type="button"
	data-vr-web-launcher
	data-media-type="image"
	data-projection="vr180"
	data-src="vr180-sbs-image.jpg"
	data-title="Temple Hall"
	data-crossorigin="anonymous">
	<img src="temple-thumb.jpg" alt="Temple Hall">
</button>

<script type="module" src="https://pages.f-40.com/VR-Web-Player/vr180player/vr180-player.js"></script>

A page can contain any number of launchers. Each launcher represents one SBS media item. A launcher click goes straight into immersive WebXR when immersive-vr is supported. When immersive WebXR is unavailable, the same click opens a modal with the left-eye fallback view.

Launcher attributes:

  • data-vr-web-launcher: required marker.
  • data-src: required media URL. For image carousels, provide at least two comma-separated image URLs.
  • data-media-type="image|video": optional when the media type can be inferred from the URL extension.
  • data-projection="vr180|plane": defaults to vr180.
  • data-title: optional display title.
  • data-carousel: optional image carousel mode.
  • data-head-lock="auto|position|none": optional positional comfort mode. It defaults to auto, which position-locks vr180 media to the headset to avoid false 6DoF parallax, while leaving plane media fixed like a screen.
  • data-poster, data-type, and data-preload: video helpers.
  • data-crossorigin: optional media CORS mode, usually anonymous for CDN media.

Use data-projection="plane" for flat 3D media on a rectangular plane:

<button
	type="button"
	data-vr-web-launcher
	data-media-type="video"
	data-projection="plane"
	data-src="flat-sbs-video.mp4"
	data-poster="poster.jpg"
	data-title="Flat 3D Demo"
	data-type="video/mp4"
	data-crossorigin="anonymous">
	<img src="poster.jpg" alt="Flat 3D Demo">
</button>

Use data-carousel for multiple SBS still images in one immersive session:

<button
	type="button"
	data-vr-web-launcher
	data-carousel
	data-media-type="image"
	data-projection="vr180"
	data-src="first-sbs-image.png, second-sbs-image.png"
	data-title="VR180 Stills"
	data-crossorigin="anonymous">
	<img src="first-thumb.jpg" alt="VR180 Stills">
</button>

[data-vr-web-player] is now an internal container created by the launcher at runtime. Authored pages should use [data-vr-web-launcher].

Media format

This version supports side-by-side media only:

  • Video: 2:1 side-by-side video using H.264 or HEVC in an mp4 file.
  • Image: side-by-side still images in browser-supported image formats such as PNG, JPEG, or WebP.

It does not support over-under, MV-HEVC, APMP, or .aivu.

How it works

When the page loads, the script binds every [data-vr-web-launcher] on the page. When the user clicks a launcher, the player checks for navigator.xr and immersive-vr support, then splits between immersive entry and fallback modal display.

  • In WebXR, vr180 maps the left and right halves of the SBS media onto the matching eyes of a 180 degree sphere. In the default auto head-lock mode, the sphere follows headset position but not headset rotation.
  • In WebXR, plane maps the left and right halves onto the matching eyes of a floating 16:9 plane.
  • Outside WebXR, both modes render only the left half of the SBS media so viewers do not see the raw double image.
  • Video controls include a loop toggle for indefinite replay.
  • Static images show only applicable controls; playback, seek, and mute controls are video-only.
  • Image carousels replace skip buttons with previous/next image controls, so you can change stills without leaving the immersive session.
  • Controller pointers and lightweight controller overlays appear after controller interaction, then fade away after a short idle period so they do not distract from viewing.
  • Control icons are embedded from Lucide SVG definitions, so no PNG icon assets are required.

Demo

Run npm run build, then open test-pages/index.html through a local web server. The test hub has three options: a local file picker for browser-selected image/video media, one bundled SBS flat 3D image, and one bundled SBS VR180 image.

The deployed test app is expected at https://pages.f-40.com/VR-Web-Player/ once the F40 Pages workflow has published a release.

For local experimentation, run:

npm run dev

This builds the TypeScript player once, then serves index.html with Vite at a local URL.

For headset testing, the page must be a secure context before the browser will expose immersive WebXR. A LAN URL such as http://192.168.x.x:5173/ is useful for checking layout and media loading, but it will usually not show the headset's immersive VR prompt. Use an HTTPS URL with a trusted certificate, a trusted tunnel, or a deployed CDN/Pages URL for immersive testing.

F40 Pages deploy

This repo includes a Gitea Actions workflow at .gitea/workflows/publish-pages.yml. It builds the test app into dist/ and uploads it to the shared F40 Pages R2 bucket using the layout:

sites/{site-name}/releases/{git-sha}-{run-attempt}/
sites/{site-name}/current.json

By default, this repo uses:

npm ci && npm run build:test-app

The build:test-app script compiles the player, copies the generated vr180player/ CDN assets, copies the simplified test-pages/ app, and optionally copies media/ if that local directory exists.

Required Gitea secrets:

  • F40_PAGES_R2_ACCOUNT_ID
  • F40_PAGES_R2_BUCKET
  • F40_PAGES_R2_ACCESS_KEY_ID
  • F40_PAGES_R2_SECRET_ACCESS_KEY

Optional Gitea variables:

  • F40_PAGES_SITE_NAME: defaults to the repository name.
  • F40_PAGES_BUILD_COMMAND: defaults to npm ci && npm run build:test-app.
  • F40_PAGES_OUTPUT_DIR: defaults to dist.

When the middleman router serves the current release at https://pages.f-40.com/{site-name}/, the generated player can also be used as a CDN from:

<script type="module" src="https://pages.f-40.com/VR-Web-Player/vr180player/vr180-player.js"></script>

For stronger CDN caching, the router can expose immutable release URLs and use the current.json file only to resolve the latest release.

Development

The player source is TypeScript in src/vr180player/. Generated JavaScript files in vr180player/ are ignored by git so CI/CD can build and publish them from source.

npm install
npm run dev
npm run build
npm run build:test-app

Edit the TypeScript source files rather than generated JavaScript. A typical CI/CD publish step should run npm ci, npm run build, then publish vr180player/ with its generated .js files and CSS.

Upload to Cloudflare R2

Copy .env.r2.example to .env.r2, then fill in your R2 account, bucket, and S3-compatible access key credentials.

npm run upload:r2:dry-run
npm run deploy:r2

By default this uploads the built vr180player/ folder under the vr180player/ object prefix. Change R2_SOURCE_DIR or R2_PREFIX in .env.r2 if you want a different source folder or CDN path.

Description
No description provided
Readme Unlicense 246 MiB
Languages
TypeScript 68.7%
JavaScript 23%
CSS 6.2%
HTML 2.1%