npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bubo-squared/gyroview

v0.8.1

Published

Play raw Insta360 .insv recordings (X3, X4, X5, X6) in the browser: a <gyro-view> web component that stitches and gyro-stabilizes the camera's dual-fisheye video on the GPU.

Readme

@bubo-squared/gyroview

Play raw .insv recordings of Insta360 cameras (X3, X4, X5, X6, ONE RS) and the Antigravity A1 in the browser. <gyro-view> reads the camera's dual-fisheye file directly, over HTTP byte ranges or from a local file, decodes both lenses in hardware with WebCodecs, and stitches and gyro-stabilizes them on the GPU. No Insta360 Studio export step.

Try it first: insv-player.com plays .insv files with this package. Drop your own recording to check that your camera and browser work before you install.

Install

npm install @bubo-squared/gyroview

It brings Three.js 0.186 and mediabunny 1 along. A page that uses Three.js 0.186 itself shares that copy; any other version means two copies in the bundle, about 128 KB gzipped. The package is ES modules only: CommonJS code loads it with import().

Use

Register the element once, then use it like a video element:

import '@bubo-squared/gyroview/define';
<gyro-view
  src="https://media.example/VID_20260814_132640_00_013.insv"
  stabilization="lock"
  controls
  muted
></gyro-view>

A relative src resolves against the document, as an image's does. timeupdate comes four times a second while playing, as a media element's does; frame comes with every picture drawn.

A page without a bundler loads the standalone file, which has Three.js and mediabunny inside (about 260 KB compressed) and registers the element; it exports what the package does:

<script
  type="module"
  src="https://cdn.jsdelivr.net/npm/@bubo-squared/[email protected]/dist/standalone.js"
></script>
<script type="module">
  import { inspectRecording } from 'https://cdn.jsdelivr.net/npm/@bubo-squared/[email protected]/dist/standalone.js';
</script>

Bundling it next to a Three.js of the page's own ships two copies; @bubo-squared/gyroview and @bubo-squared/gyroview/define share the page's instead.

Play a file the visitor picks, without any server:

const player = document.querySelector('gyro-view');
const input = document.querySelector<HTMLInputElement>('input[type="file"]');
input?.addEventListener('change', () => {
  const file = input.files?.[0];
  if (file && player) player.loadFiles({ main: file });
});

@bubo-squared/gyroview/define registers <gyro-view> when imported. To choose the moment yourself, import defineGyroView from @bubo-squared/gyroview and call it. Both are safe to import in a framework that renders on the server: there they register nothing, and the element comes alive once the page runs in the browser.

The element's events are typed: each is a CustomEvent with its payload in detail, and document.querySelector('gyro-view') is a GyroViewElement.

player?.addEventListener('ready', (event) => console.log(event.detail.model));
player?.addEventListener('error', (event) => console.log(event.detail.code));
player?.addEventListener('warning', (event) => {
  if (event.detail.code === 'autoplay-blocked') showTapToPlay();
});

Its words are English until the page gives its own, and a failure shows visitors a plain sentence while the error event carries the diagnostic:

if (player) player.messages = { labels: { play: 'Lecture' }, errors: { cors: 'Introuvable.' } };

Read what a recording holds without playing it: its trailer records, the info record (camera, firmware, frame rate), the lens calibration and summaries of the gyro and exposure records.

import { inspectRecording } from '@bubo-squared/gyroview';

const fromFile = await inspectRecording(file);
const fromUrl = await inspectRecording('https://media.example/VID_20260814_132640_00_013.insv');

For an interface of your own, createBrowserPlayer gives the player without the element: it draws on your canvas, sounds through your audio element and has the same events, which player.events.on listens to. player.togglePlayback() plays or pauses as the element's tap does, a refused start reported as a warning. attachViewGestures and attachKeyboard give your canvas the element's drags, pinches, wheel zoom and taps, and its keyboard shortcuts; each returns what removes them.

import { createBrowserPlayer } from '@bubo-squared/gyroview';

const player = createBrowserPlayer({ canvas, audio });
player.events.on('timeupdate', (time) => console.log(time));
attachViewGestures(canvas, player);
attachKeyboard(canvas, player);
await player.load({ main: { url: 'https://media.example/VID_20260814_132640_00_013.insv' } });
await player.play();
player.seek(30);
player.lookAt(90, 0);

The canvas keeps no picture once the browser has shown it: a snapshot (drawImage, toBlob) is taken in a frame listener, which runs right after each picture is drawn. There are no buffered ranges to show yet, and no playback rates other than 1.

How a recording is downloaded

A recording named by URL is read in byte ranges, in file order, as it plays: the lenses and the sound come from the same bytes, each fetched about once. Until the first play the player reads only what opening it and showing the first frame need (no frame with preload="none"). Playing, it keeps up to 10 s or 128 MiB ahead, whichever is less, shared by the two files of a split recording; paused, it reads on to that budget and stops. A seek ends the old position's requests at once. When the network falls behind the recording, playback waits until the next 4 s are downloaded, and plays on in stretches rather than a frame at a time. A range that breaks off or stalls for 10 s is asked for again from its next byte, and a recording replaced at its URL while it plays fails with source-changed. A server slow to answer each request, such as a cloud drive's download API, is asked in fewer, larger ranges, so its waits do not slow playback.

@bubo-squared/gyroview also exports GyroViewError, the list of its codes (GYRO_VIEW_ERROR_CODES, with isGyroViewErrorCode to check a string against it), and the types of the element's settings, metadata, events and inspection.

Frameworks

The element takes properties and attributes as any element does, and its events are DOM events, so a framework needs only to know the tag.

React 19 sets the attributes and hears events through a ref. For JSX to know the tag, a declaration file of the project's own extends React's elements with the package's GyroViewAttributes:

// gyro-view.d.ts
import type { DetailedHTMLProps, HTMLAttributes } from 'react';
import type { GyroViewAttributes, GyroViewElement } from '@bubo-squared/gyroview';

declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'gyro-view': DetailedHTMLProps<HTMLAttributes<GyroViewElement>, GyroViewElement> &
        GyroViewAttributes;
    }
  }
}
const player = useRef<GyroViewElement>(null);
useEffect(() => {
  const element = player.current;
  const onReady = (event: CustomEvent<PlayerMetadata>): void => console.log(event.detail.model);
  element?.addEventListener('ready', onReady);
  return () => element?.removeEventListener('ready', onReady);
}, []);
return <gyro-view ref={player} src={url} stabilization="lock" controls muted />;

A fetch for recordings behind a token is a property only, with no attribute. Set it where React commits src, in a ref callback or useLayoutEffect, not in useEffect, which runs after the load has started without it; and keep the function stable (useCallback, or defined outside the component), since only setting the same function again leaves the recording playing:

const withToken = useCallback<RecordingFetch>((url, init) => {
  const headers = new Headers(init.headers);
  headers.set('Authorization', `Bearer ${token.current}`);
  return fetch(url, { ...init, headers });
}, []);
useLayoutEffect(() => {
  if (player.current) player.current.fetch = withToken;
}, [withToken]);

Vue 3 is told the tag is a custom element (isCustomElement: (tag) => tag === 'gyro-view' in the template compiler options), then binds as usual: <gyro-view :src="url" controls @ready="onReady" />.

Svelte 5 needs nothing: <gyro-view src={url} controls onready={onReady} />.

In every case the element registers in the browser only: import @bubo-squared/gyroview/define in code that runs there, or call defineGyroView() once mounted. Importing either while the server renders is safe and registers nothing.

Several players on one page

Each player holds a WebGL context and, while a recording is loaded, two hardware video decoders. Browsers cap both (about sixteen WebGL contexts in Chrome, fewer decoders on phones), so a gallery gives its players preload="none", which keeps the decoders idle until play, and loads a recording only for the player in view, removing src from the others. An element the page removes gives its WebGL context up at once rather than when it is collected, and draws on a fresh canvas if it is added again; one whose context the browser took away while it held no recording draws on a fresh canvas at its next load.

Requirements

  • A secure page. WebCodecs exists only on https:// pages, or http://localhost; elsewhere the player fails with webcodecs-unavailable.

  • Recordings served in byte ranges. The server answers Range requests with 206, and sends CORS headers when the recordings live on another origin than the page:

    Access-Control-Allow-Origin: https://your-site.example
    Access-Control-Allow-Methods: GET, HEAD
    Access-Control-Allow-Headers: Range
    Access-Control-Expose-Headers: Content-Range, Content-Length, Accept-Ranges, ETag

    Recordings kept behind the visitor's cookies take crossorigin="use-credentials" on the element, or credentials: 'include' on a URL given to createBrowserPlayer's load, and a host that names the page's origin, never *, and adds Access-Control-Allow-Credentials: true.

    Recordings behind a token take the page's own function in the element's fetch property, or in fetch on a URL given to load: it is called in place of fetch for every request, and adds the header the host asks for.

    player.fetch = (url, init) => {
      const headers = new Headers(init.headers);
      headers.set('Authorization', `Bearer ${token()}`);
      return fetch(url, { ...init, headers });
    };
  • A hardware HEVC decoder. 5.7K plays on recent laptops and phones; 8K needs a Level 6 decoder (Apple Silicon, recent NVIDIA and Intel). On Linux, Chrome reaches the decoder only through VA-API: an Intel or AMD GPU whose driver offers HEVC, not NVIDIA's own driver or a virtual machine (chrome://gpu lists Decode hevc main under Video Acceleration Information when it can). H.264 recordings do not need it. A recording the browser cannot decode fails with the codec-unsupported error.

The supported browsers, with the oldest versions that have what the player uses (WebCodecs, WebGL 2, container queries, color-mix(), the ES2023 array methods, and on iPhone ManagedMediaSource for the sound):

| Browser | From | Notes | | --------------------- | ---- | ---------------------------------------------------------------------------------------- | | Chrome, Edge desktop | 111 | The controls' colours need 111; HEVC is decoded in hardware from 107 on (Linux: VA-API). | | Safari on macOS | 16.4 | | | Safari on iPhone/iPad | 17.1 | 16.4 to 17.0 play without sound, with a warning event. |

Firefox and Chrome on Android are untested: they play what their decoders accept.

Until 1.0, a minor version may change the API; CHANGELOG.md says what changed.

Reference

Every attribute, method, event and keyboard shortcut is in the project README, and what a failure tells a page, with what a <video> fallback can do, under When a recording cannot play; hosting and the error codes are in docs/DEPLOYMENT.md.

License

MIT