├── .browserslistrc ├── .changeset ├── README.md └── config.json ├── .env.example ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ ├── config.yml │ └── feature_internal.yml ├── content │ ├── logo-dark.svg │ └── logo-light.svg ├── dependabot.yml ├── pull_request_template.md ├── stale.yml └── workflows │ ├── cache.yml │ ├── main.yml │ ├── release.yml │ ├── snapshot.yml │ └── tag.yml ├── .gitignore ├── .node-version ├── .vscode ├── extensions.json └── settings.json ├── CODEOWNERS ├── LICENSE ├── README.md ├── SECURITY.md ├── apps ├── docs-embed │ ├── components-to-string.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── app │ │ │ ├── broadcast │ │ │ │ ├── [key] │ │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ └── loading.tsx │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── player │ │ │ │ ├── [key] │ │ │ │ └── page.tsx │ │ │ │ ├── layout.tsx │ │ │ │ └── loading.tsx │ │ ├── components │ │ │ ├── broadcast │ │ │ │ ├── audio.tsx │ │ │ │ ├── camera.tsx │ │ │ │ ├── container.tsx │ │ │ │ ├── controls.tsx │ │ │ │ ├── enabled.tsx │ │ │ │ ├── error.tsx │ │ │ │ ├── fullscreen.tsx │ │ │ │ ├── getting-started.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── pip.tsx │ │ │ │ ├── portal.tsx │ │ │ │ ├── root.tsx │ │ │ │ ├── screenshare.tsx │ │ │ │ ├── source.tsx │ │ │ │ ├── status.tsx │ │ │ │ ├── stream-key.ts │ │ │ │ ├── use-broadcast-context.tsx │ │ │ │ └── video.tsx │ │ │ ├── code │ │ │ │ ├── code-server.tsx │ │ │ │ ├── code.tsx │ │ │ │ └── shiki.css │ │ │ └── player │ │ │ │ ├── clip.tsx │ │ │ │ ├── container.tsx │ │ │ │ ├── controls.tsx │ │ │ │ ├── error.tsx │ │ │ │ ├── fullscreen.tsx │ │ │ │ ├── getting-started.tsx │ │ │ │ ├── live.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── pip.tsx │ │ │ │ ├── play.tsx │ │ │ │ ├── portal.tsx │ │ │ │ ├── poster.tsx │ │ │ │ ├── rate.tsx │ │ │ │ ├── root.tsx │ │ │ │ ├── seek.tsx │ │ │ │ ├── source.ts │ │ │ │ ├── time.tsx │ │ │ │ ├── use-media-context.tsx │ │ │ │ ├── video-quality.tsx │ │ │ │ ├── video.tsx │ │ │ │ └── volume.tsx │ │ └── lib │ │ │ ├── broadcast-components.ts │ │ │ ├── code-components.ts │ │ │ ├── components.ts │ │ │ ├── livepeer.ts │ │ │ ├── player-components.ts │ │ │ ├── shiki.ts │ │ │ ├── types.ts │ │ │ └── utils.ts │ ├── tailwind.config.ts │ └── tsconfig.json └── lvpr-tv │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── src │ ├── app │ │ ├── broadcast │ │ │ └── [key] │ │ │ │ └── page.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ └── page.tsx │ ├── components │ │ ├── IframeMessenger.tsx │ │ ├── PlayerErrorMonitor.tsx │ │ ├── broadcast │ │ │ ├── Broadcast.tsx │ │ │ └── Settings.tsx │ │ └── player │ │ │ ├── Clip.tsx │ │ │ ├── CurrentSource.tsx │ │ │ ├── ForceError.tsx │ │ │ ├── Player.tsx │ │ │ ├── Settings.tsx │ │ │ └── actions.ts │ └── lib │ │ ├── livepeer.ts │ │ ├── types.ts │ │ └── utils.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── biome.json ├── examples ├── README.md ├── next-pages │ ├── .env.example │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── components │ │ │ ├── Clip.tsx │ │ │ ├── Player.tsx │ │ │ └── Settings.tsx │ │ ├── lib │ │ │ ├── livepeer.ts │ │ │ └── utils.ts │ │ ├── pages │ │ │ ├── _app.tsx │ │ │ ├── _document.tsx │ │ │ ├── alternative-player.tsx │ │ │ ├── api │ │ │ │ ├── clip.ts │ │ │ │ └── jwt.ts │ │ │ ├── globals.css │ │ │ └── index.tsx │ │ └── public │ │ │ └── favicon.ico │ ├── tailwind.config.ts │ └── tsconfig.json ├── next │ ├── .env.example │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── src │ │ ├── app │ │ │ ├── broadcast │ │ │ │ ├── Broadcast.tsx │ │ │ │ ├── Settings.tsx │ │ │ │ └── page.tsx │ │ │ ├── favicon.ico │ │ │ ├── globals.css │ │ │ ├── layout.tsx │ │ │ ├── livepeer.ts │ │ │ ├── page.tsx │ │ │ └── player │ │ │ │ └── [type] │ │ │ │ ├── Clip.tsx │ │ │ │ ├── CurrentSource.tsx │ │ │ │ ├── ForceError.tsx │ │ │ │ ├── Player.tsx │ │ │ │ ├── Settings.tsx │ │ │ │ ├── actions.ts │ │ │ │ └── page.tsx │ │ └── lib │ │ │ └── utils.ts │ ├── tailwind.config.ts │ └── tsconfig.json └── with-pubnub │ ├── .env.example │ ├── Readme.md │ ├── next.config.js │ ├── package.json │ ├── postcss.config.js │ ├── public │ └── banned.png │ ├── src │ ├── app │ │ ├── actions.ts │ │ ├── create-livestream-button.tsx │ │ ├── favicon.ico │ │ ├── globals.css │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── view │ │ │ └── [playbackId] │ │ │ ├── PlayerWithChat.tsx │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── components │ │ ├── broadcast │ │ │ ├── Broadcast.tsx │ │ │ └── Settings.tsx │ │ ├── chat │ │ │ ├── Chat.tsx │ │ │ ├── api │ │ │ │ ├── README.md │ │ │ │ └── moderation.ts │ │ │ ├── components │ │ │ │ ├── admin.tsx │ │ │ │ ├── cards │ │ │ │ │ ├── flagged-message-card.tsx │ │ │ │ │ ├── flagged-user-card.tsx │ │ │ │ │ └── restricted-user-card.tsx │ │ │ │ ├── dropdown.tsx │ │ │ │ ├── message.tsx │ │ │ │ └── sign-in.tsx │ │ │ └── context │ │ │ │ └── ChatContext.tsx │ │ └── player │ │ │ ├── Clip.tsx │ │ │ ├── Player.tsx │ │ │ ├── Settings.tsx │ │ │ └── actions.ts │ └── lib │ │ ├── livepeer.ts │ │ └── utils.ts │ ├── tailwind.config.ts │ └── tsconfig.json ├── generate-version.ts ├── package.json ├── packages ├── core-react │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── crypto.test.ts │ │ ├── crypto.ts │ │ ├── index.test.ts │ │ └── index.ts │ ├── test │ │ ├── index.tsx │ │ └── setup.ts │ ├── tsconfig.json │ └── tsup.config.js ├── core-web │ ├── CHANGELOG.md │ ├── README.md │ ├── global-browser.d.ts │ ├── package.json │ ├── src │ │ ├── broadcast.test.ts │ │ ├── broadcast.ts │ │ ├── browser.test.ts │ │ ├── browser.ts │ │ ├── external.test.ts │ │ ├── external.ts │ │ ├── hls.test.ts │ │ ├── hls.ts │ │ ├── hls │ │ │ └── hls.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── media.test.ts │ │ ├── media.ts │ │ ├── media │ │ │ ├── controls │ │ │ │ ├── controller.ts │ │ │ │ ├── device.ts │ │ │ │ ├── fullscreen.ts │ │ │ │ ├── index.test.ts │ │ │ │ ├── index.ts │ │ │ │ ├── pictureInPicture.ts │ │ │ │ └── volume.ts │ │ │ ├── metrics.ts │ │ │ └── utils.ts │ │ ├── webrtc.test.ts │ │ ├── webrtc.ts │ │ └── webrtc │ │ │ ├── shared.ts │ │ │ ├── whep.ts │ │ │ └── whip.ts │ ├── test │ │ ├── index.ts │ │ ├── mocks.ts │ │ ├── sample.mp4 │ │ ├── setup.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── tsup.config.js ├── core │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ │ ├── crypto.test.ts │ │ ├── crypto.ts │ │ ├── crypto │ │ │ ├── ecdsa.ts │ │ │ ├── getSubtleCrypto.ts │ │ │ ├── jwt.test.ts │ │ │ ├── jwt.ts │ │ │ └── pkcs8.ts │ │ ├── errors.test.ts │ │ ├── errors.ts │ │ ├── external.test.ts │ │ ├── external.ts │ │ ├── media.test.ts │ │ ├── media.ts │ │ ├── media │ │ │ ├── controller.ts │ │ │ ├── errors.ts │ │ │ ├── external.test.ts │ │ │ ├── external.ts │ │ │ ├── metrics-new.test.ts │ │ │ ├── metrics-new.ts │ │ │ ├── metrics-utils.test.ts │ │ │ ├── metrics-utils.ts │ │ │ ├── metrics.ts │ │ │ ├── mime.ts │ │ │ ├── src.ts │ │ │ ├── storage.ts │ │ │ ├── utils.test.ts │ │ │ └── utils.ts │ │ ├── storage.test.ts │ │ ├── storage.ts │ │ ├── utils.test.ts │ │ ├── utils.ts │ │ ├── utils │ │ │ ├── deepMerge.test.ts │ │ │ ├── deepMerge.ts │ │ │ ├── omick.test.ts │ │ │ ├── omick.ts │ │ │ ├── storage │ │ │ │ ├── arweave.test.ts │ │ │ │ ├── arweave.ts │ │ │ │ ├── index.ts │ │ │ │ ├── ipfs.test.ts │ │ │ │ └── ipfs.ts │ │ │ ├── string.test.ts │ │ │ ├── string.ts │ │ │ ├── types.ts │ │ │ ├── warn.test.ts │ │ │ └── warn.ts │ │ ├── version.test.ts │ │ └── version.ts │ ├── test │ │ ├── index.ts │ │ ├── mocks.ts │ │ ├── sample.mp4 │ │ ├── setup.ts │ │ └── utils.ts │ ├── tsconfig.json │ └── tsup.config.js └── react │ ├── CHANGELOG.md │ ├── README.md │ ├── package.json │ ├── src │ ├── assets.test.ts │ ├── assets.tsx │ ├── broadcast.test.ts │ ├── broadcast.tsx │ ├── broadcast │ │ ├── AudioEnabled.tsx │ │ ├── Broadcast.tsx │ │ ├── Controls.tsx │ │ ├── Enabled.tsx │ │ ├── Screenshare.tsx │ │ ├── SourceSelect.tsx │ │ ├── StatusIndicator.tsx │ │ ├── Video.tsx │ │ ├── VideoEnabled.tsx │ │ └── context.tsx │ ├── crypto.test.ts │ ├── crypto.ts │ ├── external.test.ts │ ├── external.ts │ ├── index.test.ts │ ├── index.ts │ ├── player.test.ts │ ├── player.tsx │ ├── player │ │ ├── ClipTrigger.tsx │ │ ├── Controls.tsx │ │ ├── LiveIndicator.tsx │ │ ├── MuteTrigger.tsx │ │ ├── Play.tsx │ │ ├── Player.tsx │ │ ├── Poster.tsx │ │ ├── RateSelect.tsx │ │ ├── Seek.tsx │ │ ├── Video.tsx │ │ ├── VideoQualitySelect.tsx │ │ └── Volume.tsx │ └── shared │ │ ├── Container.tsx │ │ ├── ErrorIndicator.tsx │ │ ├── Fullscreen.tsx │ │ ├── LoadingIndicator.tsx │ │ ├── PictureInPictureTrigger.tsx │ │ ├── Portal.tsx │ │ ├── Select.tsx │ │ ├── Slider.tsx │ │ ├── Time.tsx │ │ ├── context.tsx │ │ ├── primitive.tsx │ │ └── utils.ts │ ├── test │ ├── index.tsx │ └── setup.ts │ ├── tsconfig.json │ └── tsup.config.js ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── tsconfig.json ├── turbo.json └── vitest.config.ts /.browserslistrc: -------------------------------------------------------------------------------- 1 | # Browsers that we support 2 | 3 | [production] 4 | last 2 versions 5 | > 0.2% 6 | not dead 7 | 8 | [ssr] 9 | node 12 10 | -------------------------------------------------------------------------------- /.changeset/README.md: -------------------------------------------------------------------------------- 1 | # Changesets 2 | 3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works 4 | with multi-package repos, or single-package repos to help you version and publish your code. You can 5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets) 6 | 7 | We have a quick list of common questions to get you started engaging with this project in 8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.6.3/schema.json", 3 | "changelog": ["@changesets/changelog-github", { "repo": "livepeer/ui-kit" }], 4 | "commit": false, 5 | "access": "public", 6 | "baseBranch": "main", 7 | "updateInternalDependencies": "patch", 8 | "ignore": ["example-*", "app-*"] 9 | } 10 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | STUDIO_API_KEY= 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug/issue 3 | title: "[bug]
2 |
5 |
26 | {name} 27 |
28 |24 | {name} 25 |
26 |, 17 | ) => defaultRender(ui, { ...options }); 18 | 19 | export { act, cleanup, fireEvent, screen } from "@testing-library/react"; 20 | export { getSampleVideo } from "../../core/test"; 21 | -------------------------------------------------------------------------------- /packages/core-react/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | // make dates stable across runs 4 | Date.now = vi.fn(() => new Date(Date.UTC(2022, 1, 1)).valueOf()); 5 | 6 | type ReactVersion = "17" | "18"; 7 | const reactVersion: ReactVersion = 8 |process.env.REACT_VERSION || "18"; 9 | 10 | // set up imports for React 17 11 | vi.mock("@testing-library/react-hooks", async () => { 12 | const packages = { 13 | "18": "@testing-library/react", 14 | "17": "@testing-library/react-hooks", 15 | }; 16 | 17 | return await vi.importActual(packages[reactVersion]); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/core-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "moduleResolution": "bundler", 5 | "esModuleInterop": true, 6 | "target": "ESNext", 7 | "lib": ["es2015", "dom"], 8 | "strict": true, 9 | "strictNullChecks": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/core-react/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | /** @type {import('tsup').Options} */ 4 | const options = { 5 | splitting: false, 6 | clean: true, 7 | sourcemap: true, 8 | dts: true, 9 | format: ["esm", "cjs"], 10 | }; 11 | 12 | const entrypoints = ["crypto"]; 13 | 14 | export default defineConfig([ 15 | { 16 | ...options, 17 | entry: { 18 | index: "src/index.ts", 19 | }, 20 | outDir: "dist", 21 | }, 22 | ...entrypoints.map((entrypoint) => ({ 23 | ...options, 24 | entry: { 25 | index: `src/${entrypoint}.ts`, 26 | }, 27 | outDir: `dist/${entrypoint}`, 28 | })), 29 | ]); 30 | -------------------------------------------------------------------------------- /packages/core-web/README.md: -------------------------------------------------------------------------------- 1 | # @livepeer/core-web 2 | 3 | ## Documentation 4 | 5 | The `@livepeer/core-web` package contains vanilla JS data fetching and Livepeer provider/protocol interactions for the livepeer ecosystem. For full documentation and examples, visit [docs.livepeer.org](https://docs.livepeer.org). 6 | 7 | ## Installation 8 | 9 | Install `@livepeer/core-web` and its peer dependencies. 10 | 11 | ```bash 12 | npm install @livepeer/core-web 13 | ``` 14 | 15 | ## Community 16 | 17 | Check out the following places for more livepeer-related content: 18 | 19 | - Join the [discussions on GitHub](https://github.com/livepeer/ui-kit/discussions) 20 | - Follow [@livepeer](https://twitter.com/livepeer) on Twitter 21 | - Jump into our [Discord](https://discord.gg/livepeer) 22 | -------------------------------------------------------------------------------- /packages/core-web/src/broadcast.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./broadcast"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "addBroadcastEventListeners", 9 | "createBroadcastStore", 10 | "createSilentAudioTrack", 11 | "getBroadcastDeviceInfo", 12 | ] 13 | `); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/core-web/src/browser.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./browser"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "addEventListeners", 9 | "addMediaMetrics", 10 | "canPlayMediaNatively", 11 | "getDeviceInfo", 12 | ] 13 | `); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/core-web/src/browser.ts: -------------------------------------------------------------------------------- 1 | export { 2 | addEventListeners, 3 | getDeviceInfo, 4 | type HlsConfig, 5 | } from "./media/controls"; 6 | export { addMediaMetrics } from "./media/metrics"; 7 | export { canPlayMediaNatively } from "./media/utils"; 8 | -------------------------------------------------------------------------------- /packages/core-web/src/external.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./external"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "getIngest", 9 | "getSrc", 10 | ] 11 | `); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/core-web/src/external.ts: -------------------------------------------------------------------------------- 1 | export { getIngest, getSrc } from "@livepeer/core"; 2 | export type { 3 | CloudflareStreamData, 4 | CloudflareUrlData, 5 | LivepeerAttestation, 6 | LivepeerAttestationIpfs, 7 | LivepeerAttestationStorage, 8 | LivepeerAttestations, 9 | LivepeerDomain, 10 | LivepeerMessage, 11 | LivepeerMeta, 12 | LivepeerName, 13 | LivepeerPhase, 14 | LivepeerPlaybackInfo, 15 | LivepeerPlaybackInfoType, 16 | LivepeerPlaybackPolicy, 17 | LivepeerPrimaryType, 18 | LivepeerSignatureType, 19 | LivepeerSource, 20 | LivepeerStorageStatus, 21 | LivepeerStream, 22 | LivepeerTasks, 23 | LivepeerTypeT, 24 | LivepeerVersion, 25 | } from "@livepeer/core"; 26 | -------------------------------------------------------------------------------- /packages/core-web/src/hls.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./hls"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "VIDEO_HLS_INITIALIZED_ATTRIBUTE", 9 | "createNewHls", 10 | "isHlsSupported", 11 | ] 12 | `); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/core-web/src/hls.ts: -------------------------------------------------------------------------------- 1 | export { 2 | VIDEO_HLS_INITIALIZED_ATTRIBUTE, 3 | createNewHls, 4 | isHlsSupported, 5 | type HlsError, 6 | type HlsVideoConfig, 7 | type VideoConfig, 8 | } from "./hls/hls"; 9 | -------------------------------------------------------------------------------- /packages/core-web/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "."; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "ACCESS_CONTROL_ERROR_MESSAGE", 9 | "BFRAMES_ERROR_MESSAGE", 10 | "NOT_ACCEPTABLE_ERROR_MESSAGE", 11 | "PERMISSIONS_ERROR_MESSAGE", 12 | "STREAM_OFFLINE_ERROR_MESSAGE", 13 | "STREAM_OPEN_ERROR_MESSAGE", 14 | "b64Decode", 15 | "b64Encode", 16 | "b64UrlDecode", 17 | "b64UrlEncode", 18 | "createControllerStore", 19 | "createStorage", 20 | "deepMerge", 21 | "getMediaSourceType", 22 | "isAccessControlError", 23 | "isBframesError", 24 | "isNotAcceptableError", 25 | "isPermissionsError", 26 | "isStreamOfflineError", 27 | "noopStorage", 28 | "omit", 29 | "pick", 30 | "version", 31 | ] 32 | `); 33 | }); 34 | -------------------------------------------------------------------------------- /packages/core-web/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | Address, 3 | Hash, 4 | } from "@livepeer/core"; 5 | export { 6 | ACCESS_CONTROL_ERROR_MESSAGE, 7 | BFRAMES_ERROR_MESSAGE, 8 | NOT_ACCEPTABLE_ERROR_MESSAGE, 9 | PERMISSIONS_ERROR_MESSAGE, 10 | STREAM_OFFLINE_ERROR_MESSAGE, 11 | STREAM_OPEN_ERROR_MESSAGE, 12 | isAccessControlError, 13 | isBframesError, 14 | isNotAcceptableError, 15 | isPermissionsError, 16 | isStreamOfflineError, 17 | } from "@livepeer/core/errors"; 18 | export { 19 | createControllerStore, 20 | getMediaSourceType, 21 | } from "@livepeer/core/media"; 22 | export type { 23 | AccessControlParams, 24 | AriaText, 25 | AudioSrc, 26 | AudioTrackSelector, 27 | Base64Src, 28 | ClipLength, 29 | ClipParams, 30 | ControlsState, 31 | DeviceInformation, 32 | ElementSize, 33 | HlsSrc, 34 | InitialProps, 35 | LegacyMediaMetrics, 36 | LegacyMetricsStatus, 37 | LegacyPlaybackMonitor, 38 | MediaControllerState, 39 | MediaControllerStore, 40 | MediaSizing, 41 | Metadata, 42 | ObjectFit, 43 | PlaybackError, 44 | PlaybackEvent, 45 | PlaybackRate, 46 | SessionData, 47 | SingleAudioTrackSelector, 48 | SingleTrackSelector, 49 | SingleVideoTrackSelector, 50 | Src, 51 | VideoQuality, 52 | VideoSrc, 53 | VideoTrackSelector, 54 | WebRTCSrc, 55 | } from "@livepeer/core/media"; 56 | export { 57 | createStorage, 58 | noopStorage, 59 | } from "@livepeer/core/storage"; 60 | export type { ClientStorage } from "@livepeer/core/storage"; 61 | export { 62 | b64Decode, 63 | b64Encode, 64 | b64UrlDecode, 65 | b64UrlEncode, 66 | deepMerge, 67 | omit, 68 | pick, 69 | } from "@livepeer/core/utils"; 70 | export { version } from "@livepeer/core/version"; 71 | -------------------------------------------------------------------------------- /packages/core-web/src/media.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./media"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "createControllerStore", 9 | "getMediaSourceType", 10 | ] 11 | `); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/core-web/src/media.ts: -------------------------------------------------------------------------------- 1 | export { 2 | createControllerStore, 3 | getMediaSourceType, 4 | } from "@livepeer/core/media"; 5 | export type { 6 | AccessControlParams, 7 | AriaText, 8 | AudioSrc, 9 | AudioTrackSelector, 10 | Base64Src, 11 | ClipLength, 12 | ClipParams, 13 | ControlsState, 14 | DeviceInformation, 15 | ElementSize, 16 | HlsSrc, 17 | InitialProps, 18 | LegacyMediaMetrics, 19 | LegacyMetricsStatus, 20 | LegacyPlaybackMonitor, 21 | MediaControllerState, 22 | MediaControllerStore, 23 | MediaSizing, 24 | Metadata, 25 | ObjectFit, 26 | PlaybackError, 27 | PlaybackEvent, 28 | PlaybackRate, 29 | SessionData, 30 | SingleAudioTrackSelector, 31 | SingleTrackSelector, 32 | SingleVideoTrackSelector, 33 | Src, 34 | VideoQuality, 35 | VideoSrc, 36 | VideoTrackSelector, 37 | WebRTCSrc, 38 | } from "@livepeer/core/media"; 39 | -------------------------------------------------------------------------------- /packages/core-web/src/media/controls/device.ts: -------------------------------------------------------------------------------- 1 | import type { DeviceInformation } from "@livepeer/core/media"; 2 | 3 | import { isHlsSupported } from "../../hls/hls"; 4 | import { getRTCPeerConnectionConstructor } from "../../webrtc/shared"; 5 | import { isAndroid, isIos, isMobile } from "../utils"; 6 | import { isFullscreenSupported } from "./fullscreen"; 7 | import { isPictureInPictureSupported } from "./pictureInPicture"; 8 | 9 | export const getDeviceInfo = (version: string): DeviceInformation => ({ 10 | version, 11 | isAndroid: isAndroid(), 12 | isIos: isIos(), 13 | isMobile: isMobile(), 14 | userAgent: 15 | typeof navigator !== "undefined" 16 | ? navigator.userAgent 17 | : "Node.js or unknown", 18 | screenWidth: 19 | typeof window !== "undefined" && window?.screen 20 | ? (window?.screen?.width ?? null) 21 | : null, 22 | 23 | isFullscreenSupported: isFullscreenSupported(), 24 | isWebRTCSupported: Boolean(getRTCPeerConnectionConstructor()), 25 | isPictureInPictureSupported: isPictureInPictureSupported(), 26 | isHlsSupported: isHlsSupported(), 27 | isVolumeChangeSupported: true, 28 | }); 29 | -------------------------------------------------------------------------------- /packages/core-web/src/media/controls/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "."; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "addEventListeners", 9 | "getDeviceInfo", 10 | "isPictureInPictureSupported", 11 | ] 12 | `); 13 | }); 14 | -------------------------------------------------------------------------------- /packages/core-web/src/media/controls/index.ts: -------------------------------------------------------------------------------- 1 | export { addEventListeners, type HlsConfig } from "./controller"; 2 | export { getDeviceInfo } from "./device"; 3 | export { isPictureInPictureSupported } from "./pictureInPicture"; 4 | -------------------------------------------------------------------------------- /packages/core-web/src/media/controls/volume.ts: -------------------------------------------------------------------------------- 1 | // if volume change is unsupported, the element will always return 1 2 | // similar to https://github.com/videojs/video.js/pull/7514/files 3 | export const isVolumeChangeSupported = (type: "audio" | "video") => { 4 | return new Promise ((resolve) => { 5 | if (typeof window === "undefined") { 6 | return false; 7 | } 8 | 9 | const testElement = document.createElement(type); 10 | const newVolume = 0.342; 11 | 12 | testElement.volume = newVolume; 13 | 14 | setTimeout(() => { 15 | const isSupported = testElement.volume !== 1; 16 | 17 | testElement.remove(); 18 | 19 | resolve(isSupported); 20 | }); 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/core-web/src/webrtc.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./webrtc"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "attachMediaStreamToPeerConnection", 9 | "createNewWHEP", 10 | "createNewWHIP", 11 | "getDisplayMedia", 12 | "getMediaDevices", 13 | "getUserMedia", 14 | ] 15 | `); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/core-web/src/webrtc.ts: -------------------------------------------------------------------------------- 1 | export { createNewWHEP } from "./webrtc/whep"; 2 | export { 3 | attachMediaStreamToPeerConnection, 4 | createNewWHIP, 5 | getDisplayMedia, 6 | getMediaDevices, 7 | getUserMedia, 8 | type WebRTCConnectedPayload, 9 | } from "./webrtc/whip"; 10 | -------------------------------------------------------------------------------- /packages/core-web/test/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | MockedVideoElement, 3 | MockedWebSocket, 4 | resetDateNow, 5 | waitForWebsocketOpen, 6 | } from "./mocks"; 7 | export { getSampleVideo } from "./utils"; 8 | -------------------------------------------------------------------------------- /packages/core-web/test/mocks.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | import crypto from "node:crypto"; 4 | 5 | vi.stubGlobal("crypto", { subtle: crypto.webcrypto.subtle }); 6 | 7 | // make dates stable across runs and increment each call 8 | export const resetDateNow = () => { 9 | let nowCount = 0; 10 | 11 | Date.now = vi.fn(() => 12 | new Date(Date.UTC(2022, 1, 1)).setSeconds(nowCount++).valueOf(), 13 | ); 14 | }; 15 | 16 | export const MockedWebSocket = vi.fn(() => ({ 17 | onopen: vi.fn(), 18 | onclose: vi.fn(), 19 | send: vi.fn(), 20 | })); 21 | 22 | vi.stubGlobal("WebSocket", MockedWebSocket); 23 | 24 | export const waitForWebsocketOpen = async (_websocket: WebSocket | null) => 25 | new Promise ((resolve, _reject) => { 26 | resolve(); 27 | }); 28 | 29 | export class MockedVideoElement extends HTMLVideoElement { 30 | listeners: { [key: string]: EventListenerOrEventListenerObject[] } = {}; 31 | 32 | load = vi.fn(() => { 33 | return true; 34 | }); 35 | 36 | addEventListener = vi.fn( 37 | (event: string, listener: EventListenerOrEventListenerObject) => { 38 | this.listeners[event] = [...(this.listeners[event] ?? []), listener]; 39 | }, 40 | ); 41 | removeEventListener = vi.fn( 42 | (event: string, listener: EventListenerOrEventListenerObject) => { 43 | this.listeners[event] = 44 | this.listeners[event]?.filter((l) => l !== listener) ?? []; 45 | }, 46 | ); 47 | dispatchEvent = vi.fn((e: Event) => { 48 | if (this.listeners[e.type]) { 49 | for (const listener of this.listeners[e.type] ?? []) { 50 | if (typeof listener === "function") { 51 | listener?.(e); 52 | } else { 53 | listener?.handleEvent(e); 54 | } 55 | } 56 | } 57 | 58 | return true; 59 | }); 60 | 61 | getAttribute = vi.fn(() => { 62 | return "false"; 63 | }); 64 | setAttribute = vi.fn(() => { 65 | return true; 66 | }); 67 | 68 | get duration() { 69 | return 777; 70 | } 71 | } 72 | 73 | // register the custom element 74 | customElements.define("mocked-video", MockedVideoElement, { 75 | extends: "video", 76 | }); 77 | -------------------------------------------------------------------------------- /packages/core-web/test/sample.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/ui-kit/bec14d79e8c70b26da2d362d9290f68b4f57a9f5/packages/core-web/test/sample.mp4 -------------------------------------------------------------------------------- /packages/core-web/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | Object.defineProperty(window, "localStorage", { 4 | value: { 5 | getItem: vi.fn(() => null), 6 | removeItem: vi.fn(() => null), 7 | setItem: vi.fn(() => null), 8 | }, 9 | writable: true, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/core-web/test/utils.ts: -------------------------------------------------------------------------------- 1 | import fs, { type ReadStream } from "node:fs"; 2 | import path from "node:path"; 3 | 4 | export function getSampleVideo(): { file: ReadStream; uploadSize: number } { 5 | const sampleFilePath = path.resolve(__dirname, "./sample.mp4"); 6 | 7 | const { size } = fs.statSync(sampleFilePath); 8 | const file = fs.createReadStream(sampleFilePath); 9 | return { file, uploadSize: size }; 10 | } 11 | -------------------------------------------------------------------------------- /packages/core-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "bundler", 5 | "target": "ESNext", 6 | "typeRoots": ["./global-browser"], 7 | "strict": true, 8 | "strictNullChecks": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/core-web/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | /** @type {import('tsup').Options} */ 4 | const options = { 5 | splitting: false, 6 | clean: true, 7 | sourcemap: true, 8 | dts: true, 9 | format: ["esm", "cjs"], 10 | }; 11 | 12 | const entrypoints = [ 13 | "broadcast", 14 | "browser", 15 | "external", 16 | "hls", 17 | "media", 18 | "webrtc", 19 | ]; 20 | 21 | export default defineConfig([ 22 | { 23 | ...options, 24 | entry: { 25 | index: "src/index.ts", 26 | }, 27 | outDir: "dist", 28 | }, 29 | ...entrypoints.map((entrypoint) => ({ 30 | ...options, 31 | entry: { 32 | index: `src/${entrypoint}.ts`, 33 | }, 34 | outDir: `dist/${entrypoint}`, 35 | })), 36 | ]); 37 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @livepeer/core 2 | 3 | ## Documentation 4 | 5 | The `@livepeer/core` package is used as a dependency in `livepeer` and `@livepeer/core-react` - it should not be installed directly. For full documentation and examples, visit [docs.livepeer.org](https://docs.livepeer.org). 6 | 7 | ## Community 8 | 9 | Check out the following places for more livepeer-related content: 10 | 11 | - Join the [discussions on GitHub](https://github.com/livepeer/ui-kit/discussions) 12 | - Follow [@livepeer](https://twitter.com/livepeer) on Twitter 13 | - Jump into our [Discord](https://discord.gg/livepeer) 14 | -------------------------------------------------------------------------------- /packages/core/src/crypto.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./crypto"; 4 | 5 | globalThis.crypto = crypto as Crypto; 6 | globalThis.window.crypto = crypto as Crypto; 7 | 8 | it("should expose correct exports", () => { 9 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 10 | [ 11 | "importPKCS8", 12 | "signAccessJwt", 13 | ] 14 | `); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/core/src/crypto.ts: -------------------------------------------------------------------------------- 1 | export { signAccessJwt, type SignAccessJwtOptions } from "./crypto/jwt"; 2 | export { importPKCS8 } from "./crypto/pkcs8"; 3 | -------------------------------------------------------------------------------- /packages/core/src/crypto/ecdsa.ts: -------------------------------------------------------------------------------- 1 | import { getSubtleCrypto } from "./getSubtleCrypto"; 2 | 3 | export const signEcdsaSha256 = async ( 4 | privateKey: CryptoKey, 5 | data: BufferSource, 6 | ) => { 7 | const subtleCrypto = await getSubtleCrypto(); 8 | 9 | return subtleCrypto.sign( 10 | { 11 | name: "ECDSA", 12 | hash: { name: "SHA-256" }, 13 | }, 14 | privateKey, 15 | data, 16 | ); 17 | }; 18 | -------------------------------------------------------------------------------- /packages/core/src/crypto/getSubtleCrypto.ts: -------------------------------------------------------------------------------- 1 | export const getSubtleCrypto = async () => { 2 | if (typeof crypto !== "undefined" && crypto?.subtle) { 3 | return crypto.subtle; 4 | } 5 | 6 | if (typeof globalThis?.crypto !== "undefined" && globalThis?.crypto?.subtle) { 7 | return globalThis.crypto.subtle; 8 | } 9 | 10 | try { 11 | const nodeCrypto = await import("node:crypto"); 12 | return nodeCrypto.webcrypto.subtle; 13 | } catch (error) { 14 | if (typeof window !== "undefined") { 15 | if (window?.crypto?.subtle) { 16 | return window.crypto.subtle; 17 | } 18 | 19 | throw new Error( 20 | "Browser is not in a secure context (HTTPS), cannot use SubtleCrypto: https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto", 21 | ); 22 | } 23 | 24 | throw new Error( 25 | `Failed to import Node.js crypto module: ${ 26 | (error as Error)?.message ?? "" 27 | }`, 28 | ); 29 | } 30 | }; 31 | -------------------------------------------------------------------------------- /packages/core/src/crypto/pkcs8.ts: -------------------------------------------------------------------------------- 1 | import { b64UrlDecode } from "../utils/string"; 2 | import { getSubtleCrypto } from "./getSubtleCrypto"; 3 | 4 | export const importPKCS8 = async (pkcs8: string): Promise => { 5 | if ( 6 | typeof pkcs8 !== "string" || 7 | pkcs8.indexOf("-----BEGIN PRIVATE KEY-----") !== 0 8 | ) { 9 | throw new TypeError('"pkcs8" must be PKCS8 formatted string'); 10 | } 11 | 12 | const privateKeyContents = b64UrlDecode( 13 | pkcs8.replace(/(?:-----(?:BEGIN|END) PRIVATE KEY-----|\s)/g, ""), 14 | ); 15 | 16 | if (!privateKeyContents) { 17 | throw new TypeError("Could not base64 decode private key contents."); 18 | } 19 | 20 | const subtleCrypto = await getSubtleCrypto(); 21 | 22 | return subtleCrypto.importKey( 23 | "pkcs8", 24 | new Uint8Array(privateKeyContents?.split("").map((c) => c.charCodeAt(0))), 25 | { 26 | name: "ECDSA", 27 | namedCurve: "P-256", 28 | }, 29 | false, 30 | ["sign"], 31 | ); 32 | }; 33 | -------------------------------------------------------------------------------- /packages/core/src/errors.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./errors"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "ACCESS_CONTROL_ERROR_MESSAGE", 9 | "BFRAMES_ERROR_MESSAGE", 10 | "NOT_ACCEPTABLE_ERROR_MESSAGE", 11 | "PERMISSIONS_ERROR_MESSAGE", 12 | "STREAM_OFFLINE_ERROR_MESSAGE", 13 | "STREAM_OPEN_ERROR_MESSAGE", 14 | "isAccessControlError", 15 | "isBframesError", 16 | "isNotAcceptableError", 17 | "isPermissionsError", 18 | "isStreamOfflineError", 19 | ] 20 | `); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/core/src/errors.ts: -------------------------------------------------------------------------------- 1 | export { 2 | ACCESS_CONTROL_ERROR_MESSAGE, 3 | BFRAMES_ERROR_MESSAGE, 4 | NOT_ACCEPTABLE_ERROR_MESSAGE, 5 | PERMISSIONS_ERROR_MESSAGE, 6 | STREAM_OFFLINE_ERROR_MESSAGE, 7 | STREAM_OPEN_ERROR_MESSAGE, 8 | isAccessControlError, 9 | isBframesError, 10 | isNotAcceptableError, 11 | isPermissionsError, 12 | isStreamOfflineError, 13 | } from "./media/errors"; 14 | -------------------------------------------------------------------------------- /packages/core/src/external.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./external"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "getIngest", 9 | "getSrc", 10 | ] 11 | `); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/core/src/external.ts: -------------------------------------------------------------------------------- 1 | export type Address = `0x${string}`; 2 | export type Hash = `0x${string}`; 3 | 4 | export { getIngest, getSrc } from "./media/external"; 5 | export type { 6 | CloudflareStreamData, 7 | CloudflareUrlData, 8 | LivepeerAttestation, 9 | LivepeerAttestationIpfs, 10 | LivepeerAttestationStorage, 11 | LivepeerAttestations, 12 | LivepeerDomain, 13 | LivepeerMessage, 14 | LivepeerMeta, 15 | LivepeerName, 16 | LivepeerPhase, 17 | LivepeerPlaybackInfo, 18 | LivepeerPlaybackInfoType, 19 | LivepeerPlaybackPolicy, 20 | LivepeerPrimaryType, 21 | LivepeerSignatureType, 22 | LivepeerSource, 23 | LivepeerStorageStatus, 24 | LivepeerStream, 25 | LivepeerTasks, 26 | LivepeerTypeT, 27 | LivepeerVersion, 28 | } from "./media/external"; 29 | -------------------------------------------------------------------------------- /packages/core/src/media.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./media"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "addLegacyMediaMetricsToStore", 9 | "addMetricsToStore", 10 | "calculateVideoQualityDimensions", 11 | "createControllerStore", 12 | "getBoundedVolume", 13 | "getMediaSourceType", 14 | ] 15 | `); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/core/src/media.ts: -------------------------------------------------------------------------------- 1 | export { createControllerStore } from "./media/controller"; 2 | export type { 3 | AriaText, 4 | ClipLength, 5 | ClipParams, 6 | ControlsState, 7 | DeviceInformation, 8 | ElementSize, 9 | InitialProps, 10 | MediaControllerState, 11 | MediaControllerStore, 12 | MediaSizing, 13 | Metadata, 14 | ObjectFit, 15 | PlaybackError, 16 | PlaybackRate, 17 | } from "./media/controller"; 18 | export { addLegacyMediaMetricsToStore } from "./media/metrics"; 19 | export type { 20 | LegacyMediaMetrics, 21 | LegacyMetricsStatus, 22 | LegacyPlaybackMonitor, 23 | } from "./media/metrics"; 24 | export { addMetricsToStore } from "./media/metrics-new"; 25 | export type { PlaybackEvent, SessionData } from "./media/metrics-new"; 26 | export { getMediaSourceType } from "./media/src"; 27 | export type { 28 | AccessControlParams, 29 | AudioSrc, 30 | AudioTrackSelector, 31 | Base64Src, 32 | HlsSrc, 33 | SingleAudioTrackSelector, 34 | SingleTrackSelector, 35 | SingleVideoTrackSelector, 36 | Src, 37 | VideoQuality, 38 | VideoSrc, 39 | VideoTrackSelector, 40 | WebRTCSrc, 41 | } from "./media/src"; 42 | export { 43 | calculateVideoQualityDimensions, 44 | getBoundedVolume, 45 | } from "./media/utils"; 46 | -------------------------------------------------------------------------------- /packages/core/src/media/errors.ts: -------------------------------------------------------------------------------- 1 | export const STREAM_OPEN_ERROR_MESSAGE = "stream open failed"; 2 | export const STREAM_OFFLINE_ERROR_MESSAGE = "stream is offline"; 3 | export const STREAM_WAITING_FOR_DATA_ERROR_MESSAGE = 4 | "stream is waiting for data"; 5 | export const ACCESS_CONTROL_ERROR_MESSAGE = 6 | "shutting down since this session is not allowed to view this stream"; 7 | export const BFRAMES_ERROR_MESSAGE = 8 | "metadata indicates that webrtc playback contains bframes"; 9 | export const NOT_ACCEPTABLE_ERROR_MESSAGE = 10 | "response indicates unacceptable playback protocol"; 11 | export const PERMISSIONS_ERROR_MESSAGE = 12 | "user did not allow the permissions request"; 13 | 14 | export const isStreamOfflineError = (error: Error): boolean => 15 | error.message.toLowerCase().includes(STREAM_OPEN_ERROR_MESSAGE) || 16 | error.message.toLowerCase().includes(STREAM_WAITING_FOR_DATA_ERROR_MESSAGE) || 17 | error.message.toLowerCase().includes(STREAM_OFFLINE_ERROR_MESSAGE); 18 | 19 | export const isAccessControlError = (error: Error): boolean => 20 | error.message.toLowerCase().includes(ACCESS_CONTROL_ERROR_MESSAGE); 21 | 22 | export const isBframesError = (error: Error): boolean => 23 | error.message.toLowerCase().includes(BFRAMES_ERROR_MESSAGE); 24 | 25 | export const isNotAcceptableError = (error: Error): boolean => 26 | error.message.toLowerCase().includes(NOT_ACCEPTABLE_ERROR_MESSAGE); 27 | 28 | export const isPermissionsError = (error: Error): boolean => 29 | error.message.toLowerCase().includes(PERMISSIONS_ERROR_MESSAGE); 30 | -------------------------------------------------------------------------------- /packages/core/src/media/storage.ts: -------------------------------------------------------------------------------- 1 | interface BaseStorage { 2 | getItem: (name: string) => string | null | Promise ; 3 | setItem: (name: string, value: string) => void | Promise ; 4 | removeItem: (name: string) => void | Promise ; 5 | } 6 | 7 | export type ClientStorage = { 8 | getItem: (key: string, defaultState?: T | null) => Promise ; 9 | setItem: (key: string, value: T | null) => Promise ; 10 | removeItem: (key: string) => Promise ; 11 | }; 12 | 13 | export const noopStorage: BaseStorage = { 14 | getItem: (_key) => "", 15 | setItem: (_key, _value) => { 16 | // 17 | }, 18 | removeItem: (_key) => { 19 | // 20 | }, 21 | }; 22 | 23 | export function createStorage({ 24 | storage = noopStorage, 25 | key: prefix = "livepeer", 26 | }: { 27 | storage?: BaseStorage; 28 | key?: string; 29 | }): ClientStorage { 30 | return { 31 | getItem: async (key, defaultState = null) => { 32 | try { 33 | const value = await storage.getItem(`${prefix}.${key}`); 34 | return value ? JSON.parse(value) : defaultState; 35 | } catch (error) { 36 | console.warn(error); 37 | return defaultState; 38 | } 39 | }, 40 | setItem: async (key, value) => { 41 | if (value === null) { 42 | await storage.removeItem(`${prefix}.${key}`); 43 | } else { 44 | try { 45 | await storage.setItem(`${prefix}.${key}`, JSON.stringify(value)); 46 | } catch (err) { 47 | console.error(err); 48 | } 49 | } 50 | }, 51 | removeItem: async (key) => storage.removeItem(`${prefix}.${key}`), 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /packages/core/src/media/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { getFormattedHoursMinutesSeconds } from "./utils"; 3 | 4 | describe("utils", () => { 5 | describe("getFormattedHoursMinutesSeconds()", () => { 6 | it("formats a value under a minute", () => { 7 | const formatted = getFormattedHoursMinutesSeconds(22); 8 | 9 | expect(formatted).toMatchInlineSnapshot('"0:22"'); 10 | }); 11 | 12 | it("formats a value over a minute", () => { 13 | const formatted = getFormattedHoursMinutesSeconds(66); 14 | 15 | expect(formatted).toMatchInlineSnapshot('"1:06"'); 16 | }); 17 | 18 | it("formats a value over 10 minutes", () => { 19 | const formatted = getFormattedHoursMinutesSeconds(660); 20 | 21 | expect(formatted).toMatchInlineSnapshot('"11:00"'); 22 | }); 23 | 24 | it("formats a value over one hour", () => { 25 | const formatted = getFormattedHoursMinutesSeconds(3601); 26 | 27 | expect(formatted).toMatchInlineSnapshot('"1:00:01"'); 28 | }); 29 | 30 | it("formats a value over 7 hours", () => { 31 | const formatted = getFormattedHoursMinutesSeconds(25201); 32 | 33 | expect(formatted).toMatchInlineSnapshot('"7:00:01"'); 34 | }); 35 | 36 | it("formats a value = 7 hours and 1 minute", () => { 37 | const formatted = getFormattedHoursMinutesSeconds(25260); 38 | 39 | expect(formatted).toMatchInlineSnapshot('"7:01:00"'); 40 | }); 41 | 42 | it("formats a value = 7 hours and 1 minute and 5 sec", () => { 43 | const formatted = getFormattedHoursMinutesSeconds(25265); 44 | 45 | expect(formatted).toMatchInlineSnapshot('"7:01:05"'); 46 | }); 47 | 48 | it("formats a null value", () => { 49 | const formatted = getFormattedHoursMinutesSeconds(null); 50 | 51 | expect(formatted).toMatchInlineSnapshot('"0:00"'); 52 | }); 53 | 54 | it("formats a NaN value", () => { 55 | const formatted = getFormattedHoursMinutesSeconds(Number.NaN); 56 | 57 | expect(formatted).toMatchInlineSnapshot('"0:00"'); 58 | }); 59 | 60 | it("formats an undefined value", () => { 61 | const formatted = getFormattedHoursMinutesSeconds(undefined); 62 | 63 | expect(formatted).toMatchInlineSnapshot('"0:00"'); 64 | }); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /packages/core/src/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { createStorage } from "./storage"; 4 | 5 | describe("createStorage", () => { 6 | it("inits", () => { 7 | const storage = createStorage({ storage: window.localStorage }); 8 | expect(storage).toBeDefined(); 9 | }); 10 | 11 | it("getItem", () => { 12 | const storage = createStorage({ storage: window.localStorage }); 13 | storage.getItem("foo"); 14 | expect(window.localStorage.getItem).toHaveBeenCalledTimes(1); 15 | expect(window.localStorage.getItem).toHaveBeenCalledWith("livepeer.foo"); 16 | }); 17 | 18 | it("setItem", () => { 19 | const storage = createStorage({ storage: window.localStorage }); 20 | storage.setItem("foo", "bar"); 21 | expect(window.localStorage.setItem).toHaveBeenCalledTimes(1); 22 | expect(window.localStorage.setItem).toHaveBeenCalledWith( 23 | "livepeer.foo", 24 | '"bar"', 25 | ); 26 | }); 27 | 28 | it("removeItem", () => { 29 | const storage = createStorage({ storage: window.localStorage }); 30 | storage.removeItem("foo"); 31 | expect(window.localStorage.removeItem).toHaveBeenCalledTimes(1); 32 | expect(window.localStorage.removeItem).toHaveBeenCalledWith("livepeer.foo"); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/core/src/storage.ts: -------------------------------------------------------------------------------- 1 | export { createStorage, noopStorage } from "./media/storage"; 2 | export type { ClientStorage } from "./media/storage"; 3 | -------------------------------------------------------------------------------- /packages/core/src/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./utils"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "b64Decode", 9 | "b64Encode", 10 | "b64UrlDecode", 11 | "b64UrlEncode", 12 | "deepMerge", 13 | "noop", 14 | "omit", 15 | "parseArweaveTxId", 16 | "parseCid", 17 | "pick", 18 | "warn", 19 | ] 20 | `); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/core/src/utils.ts: -------------------------------------------------------------------------------- 1 | export { deepMerge } from "./utils/deepMerge"; 2 | export { omit, pick } from "./utils/omick"; 3 | export { parseArweaveTxId, parseCid } from "./utils/storage"; 4 | export { 5 | b64Decode, 6 | b64Encode, 7 | b64UrlDecode, 8 | b64UrlEncode, 9 | } from "./utils/string"; 10 | export { noop } from "./utils/types"; 11 | export { warn } from "./utils/warn"; 12 | -------------------------------------------------------------------------------- /packages/core/src/utils/omick.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a new object containing only the specified keys 3 | */ 4 | export const pick = ( 5 | obj: T, 6 | ...keys: readonly K[] 7 | ): Pick => { 8 | try { 9 | const objectKeys = Object.keys(obj); 10 | 11 | return keys 12 | .filter((key) => objectKeys.includes(key as string)) 13 | .reduce( 14 | (prev, curr) => ({ 15 | // biome-ignore lint/performance/noAccumulatingSpread: 16 | ...prev, 17 | [curr]: obj[curr], 18 | }), 19 | {}, 20 | ) as Pick ; 21 | } catch (e) { 22 | throw new Error("Could not pick keys for object."); 23 | } 24 | }; 25 | 26 | /** 27 | * Create a new object excluding the specified keys 28 | */ 29 | export function omit ( 30 | obj: T, 31 | ...keys: readonly K[] 32 | ): Omit { 33 | try { 34 | const objectKeys = Object.keys(obj); 35 | 36 | return objectKeys 37 | .filter((objectKey) => !keys.some((key) => String(key) === objectKey)) 38 | .reduce( 39 | (prev, curr) => ({ 40 | // biome-ignore lint/performance/noAccumulatingSpread: 41 | ...prev, 42 | [curr]: obj[curr as K], 43 | }), 44 | {}, 45 | ) as Omit ; 46 | } catch (e) { 47 | throw new Error("Could not omit keys for object."); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/core/src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | export { parseArweaveTxId } from "./arweave"; 2 | export { parseCid } from "./ipfs"; 3 | -------------------------------------------------------------------------------- /packages/core/src/utils/string.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | 3 | import { b64Decode, b64Encode, b64UrlDecode, b64UrlEncode } from "./string"; 4 | 5 | describe("b64", () => { 6 | describe("default", () => { 7 | describe("encodes", () => { 8 | it("correctly encodes a precomputed value", () => { 9 | expect(b64Encode("somevalue")).toMatchInlineSnapshot('"c29tZXZhbHVl"'); 10 | }); 11 | }); 12 | 13 | describe("decodes", () => { 14 | it("correctly decodes a precomputed value", () => { 15 | expect(b64Decode("c29tZXZhbHVl")).toMatchInlineSnapshot('"somevalue"'); 16 | }); 17 | 18 | it("returns null on invalid value", () => { 19 | expect(b64Decode("------")).toEqual(null); 20 | }); 21 | 22 | it("returns null on invalid value", () => { 23 | expect(b64Decode("\\\\\\")).toEqual(null); 24 | }); 25 | }); 26 | }); 27 | 28 | describe("url", () => { 29 | describe("encodes", () => { 30 | it("correctly encodes a precomputed value", () => { 31 | expect(b64UrlEncode("somevalue")).toMatchInlineSnapshot( 32 | '"c29tZXZhbHVl"', 33 | ); 34 | }); 35 | }); 36 | 37 | describe("decodes", () => { 38 | it("correctly decodes a precomputed value", () => { 39 | expect(b64UrlDecode("c29tZXZhbHVl")).toMatchInlineSnapshot( 40 | '"somevalue"', 41 | ); 42 | }); 43 | 44 | it("returns null on invalid value", () => { 45 | expect(b64UrlDecode("\\\\\\")).toEqual(null); 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /packages/core/src/utils/string.ts: -------------------------------------------------------------------------------- 1 | export const b64Encode = (input: string): string | null => { 2 | try { 3 | if (typeof window !== "undefined" && "btoa" in window) { 4 | return window?.btoa?.(input) ?? null; 5 | } 6 | return Buffer?.from(input, "binary")?.toString("base64") ?? null; 7 | } catch (e) { 8 | return null; 9 | } 10 | }; 11 | 12 | export const b64Decode = (input: string): string | null => { 13 | try { 14 | if (typeof window !== "undefined" && "atob" in window) { 15 | return window?.atob?.(input) ?? null; 16 | } 17 | return Buffer?.from(input, "base64")?.toString("binary") ?? null; 18 | } catch (e) { 19 | return null; 20 | } 21 | }; 22 | 23 | export const b64UrlEncode = (input: string): string | null => { 24 | return escapeInput(b64Encode(input)); 25 | }; 26 | 27 | export const b64UrlDecode = (input: string): string | null => { 28 | const unescaped = unescapeInput(input); 29 | if (unescaped) { 30 | return b64Decode(unescaped); 31 | } 32 | return null; 33 | }; 34 | 35 | const unescapeInput = (input: string | undefined | null) => { 36 | return input 37 | ? (input + "===".slice((input.length + 3) % 4)) 38 | .replace(/-/g, "+") 39 | .replace(/_/g, "/") 40 | : null; 41 | }; 42 | 43 | const escapeInput = (input: string | undefined | null) => { 44 | return ( 45 | input?.replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") ?? null 46 | ); 47 | }; 48 | -------------------------------------------------------------------------------- /packages/core/src/utils/types.ts: -------------------------------------------------------------------------------- 1 | // biome-ignore lint/suspicious/noExplicitAny: any 2 | export const noop = (..._args: any[]) => { 3 | // 4 | }; 5 | -------------------------------------------------------------------------------- /packages/core/src/utils/warn.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it, vi } from "vitest"; 2 | 3 | import { warn } from "./warn"; 4 | 5 | describe("warn", () => { 6 | describe("args", () => { 7 | it("message", () => { 8 | console.warn = vi.fn(); 9 | const message = "foo bar baz"; 10 | warn(message); 11 | expect(console.warn).toBeCalledWith(message); 12 | }); 13 | 14 | it("id", () => { 15 | console.warn = vi.fn(); 16 | const message = "the quick brown fox"; 17 | warn(message); 18 | warn(message, "repeat"); 19 | expect(console.warn).toBeCalledWith(message); 20 | expect(console.warn).toBeCalledTimes(2); 21 | }); 22 | }); 23 | 24 | describe("behavior", () => { 25 | it("calls only once per message", () => { 26 | console.warn = vi.fn(); 27 | const message = "hello world"; 28 | warn(message); 29 | warn(message); 30 | expect(console.warn).toBeCalledWith(message); 31 | expect(console.warn).toBeCalledTimes(1); 32 | }); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /packages/core/src/utils/warn.ts: -------------------------------------------------------------------------------- 1 | const cache = new Set (); 2 | 3 | export function warn(message: string, id?: string) { 4 | if (!cache.has(id ?? message)) { 5 | console.warn(message); 6 | cache.add(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/version.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./version"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "version", 9 | ] 10 | `); 11 | }); 12 | -------------------------------------------------------------------------------- /packages/core/src/version.ts: -------------------------------------------------------------------------------- 1 | const core = "@livepeer/core@3.3.0"; 2 | const react = "@livepeer/react@4.3.3"; 3 | 4 | export const version = { 5 | core, 6 | react, 7 | } as const; 8 | -------------------------------------------------------------------------------- /packages/core/test/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | MockedVideoElement, 3 | MockedWebSocket, 4 | resetDateNow, 5 | waitForWebsocketOpen, 6 | } from "./mocks"; 7 | export { getSampleVideo } from "./utils"; 8 | -------------------------------------------------------------------------------- /packages/core/test/mocks.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | // make dates stable across runs and increment each call 4 | export const resetDateNow = () => { 5 | let nowCount = 0; 6 | 7 | Date.now = vi.fn(() => 8 | new Date(Date.UTC(2022, 1, 1)).setSeconds(nowCount++).valueOf(), 9 | ); 10 | }; 11 | 12 | export const MockedWebSocket = vi.fn(() => ({ 13 | onopen: vi.fn(), 14 | onclose: vi.fn(), 15 | send: vi.fn(), 16 | })); 17 | 18 | vi.stubGlobal("WebSocket", MockedWebSocket); 19 | 20 | export const waitForWebsocketOpen = async (_websocket: WebSocket | null) => 21 | new Promise ((resolve, _reject) => { 22 | resolve(); 23 | }); 24 | 25 | export class MockedVideoElement extends HTMLVideoElement { 26 | listeners: { [key: string]: EventListenerOrEventListenerObject[] } = {}; 27 | 28 | load = vi.fn(() => { 29 | return true; 30 | }); 31 | 32 | addEventListener = vi.fn( 33 | (event: string, listener: EventListenerOrEventListenerObject) => { 34 | this.listeners[event] = [...(this.listeners[event] ?? []), listener]; 35 | }, 36 | ); 37 | removeEventListener = vi.fn( 38 | (event: string, listener: EventListenerOrEventListenerObject) => { 39 | this.listeners[event] = 40 | this.listeners[event]?.filter((l) => l !== listener) ?? []; 41 | }, 42 | ); 43 | dispatchEvent = vi.fn((e: Event) => { 44 | if (this.listeners[e.type]) { 45 | for (const listener of this.listeners[e.type] ?? []) { 46 | if (typeof listener === "function") { 47 | listener?.(e); 48 | } else { 49 | listener?.handleEvent(e); 50 | } 51 | } 52 | } 53 | 54 | return true; 55 | }); 56 | 57 | getAttribute = vi.fn(() => { 58 | return "false"; 59 | }); 60 | setAttribute = vi.fn(() => { 61 | return true; 62 | }); 63 | 64 | get duration() { 65 | return 777; 66 | } 67 | } 68 | 69 | // register the custom element 70 | customElements.define("mocked-video", MockedVideoElement, { 71 | extends: "video", 72 | }); 73 | -------------------------------------------------------------------------------- /packages/core/test/sample.mp4: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/livepeer/ui-kit/bec14d79e8c70b26da2d362d9290f68b4f57a9f5/packages/core/test/sample.mp4 -------------------------------------------------------------------------------- /packages/core/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | Object.defineProperty(window, "localStorage", { 4 | value: { 5 | getItem: vi.fn(() => null), 6 | removeItem: vi.fn(() => null), 7 | setItem: vi.fn(() => null), 8 | }, 9 | writable: true, 10 | }); 11 | -------------------------------------------------------------------------------- /packages/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "ESNext", 4 | "moduleResolution": "bundler", 5 | "target": "ESNext", 6 | "strict": true, 7 | "strictNullChecks": true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/core/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | /** @type {import('tsup').Options} */ 4 | const options = { 5 | splitting: false, 6 | clean: true, 7 | sourcemap: true, 8 | dts: true, 9 | format: ["esm", "cjs"], 10 | }; 11 | 12 | const entrypoints = [ 13 | "crypto", 14 | "errors", 15 | "media", 16 | "storage", 17 | "utils", 18 | "version", 19 | ]; 20 | 21 | export default defineConfig([ 22 | { 23 | ...options, 24 | entry: { 25 | index: "src/external.ts", 26 | }, 27 | outDir: "dist", 28 | }, 29 | ...entrypoints.map((entrypoint) => ({ 30 | ...options, 31 | entry: { 32 | index: `src/${entrypoint}.ts`, 33 | }, 34 | outDir: `dist/${entrypoint}`, 35 | })), 36 | ]); 37 | -------------------------------------------------------------------------------- /packages/react/README.md: -------------------------------------------------------------------------------- 1 | # @livepeer/react 2 | 3 | ## Documentation 4 | 5 | The `@livepeer/react` package contains React-specific components for the livepeer ecosystem. For full documentation and examples, visit [docs.livepeer.org](https://docs.livepeer.org). 6 | 7 | ## Installation 8 | 9 | Install `@livepeer/react` using a package manager. 10 | 11 | ```bash 12 | npm install @livepeer/react 13 | ``` 14 | 15 | ## Community 16 | 17 | Check out the following places for more livepeer-related content: 18 | 19 | - Join the [discussions on GitHub](https://github.com/livepeer/ui-kit/discussions) 20 | - Follow [@livepeer](https://twitter.com/livepeer) on Twitter 21 | - Jump into our [Discord](https://discord.gg/livepeer) 22 | -------------------------------------------------------------------------------- /packages/react/src/assets.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./assets"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "ClipIcon", 9 | "DisableAudioIcon", 10 | "DisableVideoIcon", 11 | "EnableAudioIcon", 12 | "EnableVideoIcon", 13 | "EnterFullscreenIcon", 14 | "ExitFullscreenIcon", 15 | "LoadingIcon", 16 | "MuteIcon", 17 | "OfflineErrorIcon", 18 | "PauseIcon", 19 | "PictureInPictureIcon", 20 | "PlayIcon", 21 | "PrivateErrorIcon", 22 | "SettingsIcon", 23 | "StartScreenshareIcon", 24 | "StopIcon", 25 | "StopScreenshareIcon", 26 | "UnmuteIcon", 27 | ] 28 | `); 29 | }); 30 | -------------------------------------------------------------------------------- /packages/react/src/broadcast.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./broadcast"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "AudioEnabledIndicator", 9 | "AudioEnabledTrigger", 10 | "BroadcastProvider", 11 | "Container", 12 | "Controls", 13 | "EnabledIndicator", 14 | "EnabledTrigger", 15 | "ErrorIndicator", 16 | "FullscreenIndicator", 17 | "FullscreenTrigger", 18 | "LoadingIndicator", 19 | "MediaProvider", 20 | "PictureInPictureTrigger", 21 | "Portal", 22 | "Range", 23 | "Root", 24 | "ScreenshareIndicator", 25 | "ScreenshareTrigger", 26 | "SelectArrow", 27 | "SelectContent", 28 | "SelectGroup", 29 | "SelectIcon", 30 | "SelectItem", 31 | "SelectItemIndicator", 32 | "SelectItemText", 33 | "SelectLabel", 34 | "SelectPortal", 35 | "SelectRoot", 36 | "SelectScrollDownButton", 37 | "SelectScrollUpButton", 38 | "SelectSeparator", 39 | "SelectTrigger", 40 | "SelectValue", 41 | "SelectViewport", 42 | "SourceSelect", 43 | "StatusIndicator", 44 | "Thumb", 45 | "Time", 46 | "Track", 47 | "Video", 48 | "VideoEnabledIndicator", 49 | "VideoEnabledTrigger", 50 | "createBroadcastScope", 51 | "createMediaScope", 52 | "useBroadcastContext", 53 | "useMediaContext", 54 | "useStore", 55 | ] 56 | `); 57 | }); 58 | -------------------------------------------------------------------------------- /packages/react/src/broadcast/context.tsx: -------------------------------------------------------------------------------- 1 | import type { BroadcastStore } from "@livepeer/core-web/broadcast"; 2 | import { createContextScope } from "@radix-ui/react-context"; 3 | 4 | import type { Scope } from "@radix-ui/react-context"; 5 | 6 | const MEDIA_NAME = "Broadcast"; 7 | 8 | // biome-ignore lint/complexity/noBannedTypes: allow {} 9 | type BroadcastScopedProps = P & { __scopeBroadcast?: Scope }; 10 | const [createBroadcastContext, createBroadcastScope] = 11 | createContextScope(MEDIA_NAME); 12 | 13 | type BroadcastContextValue = { 14 | store: BroadcastStore; 15 | }; 16 | 17 | const [BroadcastProvider, useBroadcastContext] = 18 | createBroadcastContext
(MEDIA_NAME); 19 | 20 | export { BroadcastProvider, createBroadcastScope, useBroadcastContext }; 21 | export type { BroadcastContextValue, BroadcastScopedProps }; 22 | -------------------------------------------------------------------------------- /packages/react/src/crypto.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./crypto"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "importPKCS8", 9 | "signAccessJwt", 10 | ] 11 | `); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/react/src/crypto.ts: -------------------------------------------------------------------------------- 1 | export { 2 | importPKCS8, 3 | signAccessJwt, 4 | type SignAccessJwtOptions, 5 | } from "@livepeer/core/crypto"; 6 | -------------------------------------------------------------------------------- /packages/react/src/external.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./external"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "getIngest", 9 | "getSrc", 10 | ] 11 | `); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/react/src/external.ts: -------------------------------------------------------------------------------- 1 | export { getIngest, getSrc } from "@livepeer/core"; 2 | export type { 3 | CloudflareStreamData, 4 | CloudflareUrlData, 5 | LivepeerAttestation, 6 | LivepeerAttestationIpfs, 7 | LivepeerAttestationStorage, 8 | LivepeerAttestations, 9 | LivepeerDomain, 10 | LivepeerMessage, 11 | LivepeerMeta, 12 | LivepeerName, 13 | LivepeerPhase, 14 | LivepeerPlaybackInfo, 15 | LivepeerPlaybackInfoType, 16 | LivepeerPlaybackPolicy, 17 | LivepeerPrimaryType, 18 | LivepeerSignatureType, 19 | LivepeerSource, 20 | LivepeerStorageStatus, 21 | LivepeerStream, 22 | LivepeerTasks, 23 | LivepeerTypeT, 24 | LivepeerVersion, 25 | } from "@livepeer/core"; 26 | -------------------------------------------------------------------------------- /packages/react/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "createControllerStore", 9 | "createStorage", 10 | "getMediaSourceType", 11 | "noopStorage", 12 | "version", 13 | ] 14 | `); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/react/src/index.ts: -------------------------------------------------------------------------------- 1 | export type { 2 | AudioDeviceId, 3 | BroadcastAriaText, 4 | BroadcastControlsState, 5 | BroadcastDeviceInformation, 6 | BroadcastState, 7 | BroadcastStatus, 8 | BroadcastStore, 9 | InitialBroadcastProps, 10 | MediaDeviceIds, 11 | MediaDeviceInfoExtended, 12 | VideoDeviceId, 13 | } from "@livepeer/core-web/broadcast"; 14 | export { 15 | createControllerStore, 16 | getMediaSourceType, 17 | } from "@livepeer/core/media"; 18 | export type { 19 | AccessControlParams, 20 | AriaText, 21 | AudioSrc, 22 | AudioTrackSelector, 23 | Base64Src, 24 | ClipLength, 25 | ClipParams, 26 | ControlsState, 27 | DeviceInformation, 28 | ElementSize, 29 | HlsSrc, 30 | InitialProps, 31 | LegacyMediaMetrics, 32 | LegacyMetricsStatus, 33 | LegacyPlaybackMonitor, 34 | MediaControllerState, 35 | MediaControllerStore, 36 | MediaSizing, 37 | Metadata, 38 | ObjectFit, 39 | PlaybackError, 40 | PlaybackEvent, 41 | PlaybackRate, 42 | SessionData, 43 | SingleAudioTrackSelector, 44 | SingleTrackSelector, 45 | SingleVideoTrackSelector, 46 | Src, 47 | VideoQuality, 48 | VideoSrc, 49 | VideoTrackSelector, 50 | WebRTCSrc, 51 | } from "@livepeer/core/media"; 52 | export { 53 | createStorage, 54 | noopStorage, 55 | } from "@livepeer/core/storage"; 56 | export type { ClientStorage } from "@livepeer/core/storage"; 57 | export { version } from "@livepeer/core/version"; 58 | -------------------------------------------------------------------------------- /packages/react/src/player.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, it } from "vitest"; 2 | 3 | import * as Exports from "./player"; 4 | 5 | it("should expose correct exports", () => { 6 | expect(Object.keys(Exports).sort()).toMatchInlineSnapshot(` 7 | [ 8 | "ClipTrigger", 9 | "Container", 10 | "Controls", 11 | "ErrorIndicator", 12 | "FullscreenIndicator", 13 | "FullscreenTrigger", 14 | "LiveIndicator", 15 | "LoadingIndicator", 16 | "MediaProvider", 17 | "MuteTrigger", 18 | "PictureInPictureTrigger", 19 | "PlayPauseTrigger", 20 | "PlayingIndicator", 21 | "Portal", 22 | "Poster", 23 | "Range", 24 | "RateSelect", 25 | "RateSelectItem", 26 | "Root", 27 | "Seek", 28 | "SeekBuffer", 29 | "SelectArrow", 30 | "SelectContent", 31 | "SelectGroup", 32 | "SelectIcon", 33 | "SelectItemIndicator", 34 | "SelectItemText", 35 | "SelectLabel", 36 | "SelectPortal", 37 | "SelectScrollDownButton", 38 | "SelectScrollUpButton", 39 | "SelectSeparator", 40 | "SelectTrigger", 41 | "SelectValue", 42 | "SelectViewport", 43 | "Thumb", 44 | "Time", 45 | "Track", 46 | "Video", 47 | "VideoQualitySelect", 48 | "VideoQualitySelectItem", 49 | "Volume", 50 | "VolumeIndicator", 51 | "createMediaScope", 52 | "useMediaContext", 53 | "useStore", 54 | ] 55 | `); 56 | }); 57 | -------------------------------------------------------------------------------- /packages/react/src/player/LiveIndicator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useMemo } from "react"; 4 | 5 | import { Presence } from "@radix-ui/react-presence"; 6 | import { useStore } from "zustand"; 7 | import { type MediaScopedProps, useMediaContext } from "../shared/context"; 8 | import * as Radix from "../shared/primitive"; 9 | 10 | const LIVE_INDICATOR_NAME = "LiveIndicator"; 11 | 12 | type LiveIndicatorElement = React.ElementRef ; 13 | 14 | interface LiveIndicatorProps 15 | extends Radix.ComponentPropsWithoutRef { 16 | /** 17 | * Used to force mounting when more control is needed. Useful when 18 | * controlling animation with React animation libraries. 19 | */ 20 | forceMount?: true; 21 | /** The matcher used to determine whether the element should be shown, given the `live` state (true for live streams, and false for assets). Defaults to `true`. */ 22 | matcher?: boolean | ((live: boolean) => boolean); 23 | } 24 | 25 | const LiveIndicator = React.forwardRef< 26 | LiveIndicatorElement, 27 | LiveIndicatorProps 28 | >((props: MediaScopedProps , forwardedRef) => { 29 | const { 30 | __scopeMedia, 31 | forceMount, 32 | matcher = true, 33 | ...liveIndicatorProps 34 | } = props; 35 | 36 | const context = useMediaContext(LIVE_INDICATOR_NAME, __scopeMedia); 37 | 38 | const live = useStore(context.store, ({ live }) => live); 39 | 40 | const isPresent = useMemo( 41 | () => (typeof matcher === "function" ? matcher(live) : matcher === live), 42 | [matcher, live], 43 | ); 44 | 45 | return ( 46 | 47 | 56 | ); 57 | }); 58 | 59 | LiveIndicator.displayName = LIVE_INDICATOR_NAME; 60 | 61 | export { LiveIndicator }; 62 | export type { LiveIndicatorProps }; 63 | -------------------------------------------------------------------------------- /packages/react/src/player/MuteTrigger.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { composeEventHandlers } from "@radix-ui/primitive"; 4 | 5 | import React from "react"; 6 | 7 | import { useStore } from "zustand"; 8 | import { type MediaScopedProps, useMediaContext } from "../shared/context"; 9 | 10 | import { useShallow } from "zustand/react/shallow"; 11 | import * as Radix from "../shared/primitive"; 12 | import { noPropagate } from "../shared/utils"; 13 | 14 | const MUTE_TRIGGER_NAME = "MuteTrigger"; 15 | 16 | type MuteTriggerElement = React.ElementRef55 | ; 17 | 18 | interface MuteTriggerProps 19 | extends Radix.ComponentPropsWithoutRef {} 20 | 21 | const MuteTrigger = React.forwardRef ( 22 | (props: MediaScopedProps , forwardedRef) => { 23 | const { __scopeMedia, ...playProps } = props; 24 | 25 | const context = useMediaContext(MUTE_TRIGGER_NAME, __scopeMedia); 26 | 27 | const { muted, toggleMute } = useStore( 28 | context.store, 29 | useShallow(({ __controls, __controlsFunctions }) => ({ 30 | muted: __controls.muted, 31 | toggleMute: __controlsFunctions.requestToggleMute, 32 | })), 33 | ); 34 | 35 | const title = React.useMemo( 36 | () => (muted ? "Unmute (m)" : "Mute (m)"), 37 | [muted], 38 | ); 39 | 40 | return ( 41 | 52 | ); 53 | }, 54 | ); 55 | 56 | MuteTrigger.displayName = MUTE_TRIGGER_NAME; 57 | 58 | export { MuteTrigger }; 59 | export type { MuteTriggerProps }; 60 | -------------------------------------------------------------------------------- /packages/react/src/player/Poster.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { Presence } from "@radix-ui/react-presence"; 6 | import { useStore } from "zustand"; 7 | import { type MediaScopedProps, useMediaContext } from "../shared/context"; 8 | import * as Radix from "../shared/primitive"; 9 | 10 | const POSTER_NAME = "Poster"; 11 | 12 | type PosterElement = React.ElementRef ; 13 | 14 | interface PosterProps 15 | extends Radix.ComponentPropsWithoutRef { 16 | /** 17 | * Used to force mounting when more control is needed. Useful when 18 | * controlling animation with React animation libraries. 19 | */ 20 | forceMount?: true; 21 | } 22 | 23 | const Poster = React.forwardRef ( 24 | (props: MediaScopedProps , forwardedRef) => { 25 | const { __scopeMedia, forceMount, src, ...posterProps } = props; 26 | 27 | const context = useMediaContext(POSTER_NAME, __scopeMedia); 28 | 29 | const poster = useStore(context.store, ({ poster }) => poster); 30 | 31 | return ( 32 | 33 | 43 | ); 44 | }, 45 | ); 46 | 47 | Poster.displayName = POSTER_NAME; 48 | 49 | export { Poster }; 50 | export type { PosterProps }; 51 | -------------------------------------------------------------------------------- /packages/react/src/shared/LoadingIndicator.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useMemo } from "react"; 4 | 5 | import { Presence } from "@radix-ui/react-presence"; 6 | import { useStore } from "zustand"; 7 | import { type MediaScopedProps, useMediaContext } from "./context"; 8 | import * as Radix from "./primitive"; 9 | 10 | const LOADING_INDICATOR_NAME = "LoadingIndicator"; 11 | 12 | type LoadingIndicatorElement = React.ElementRef 42 |; 13 | 14 | interface LoadingIndicatorProps 15 | extends Radix.ComponentPropsWithoutRef { 16 | /** 17 | * Used to force mounting when more control is needed. Useful when 18 | * controlling animation with React animation libraries. 19 | */ 20 | forceMount?: true; 21 | /** The matcher used to determine whether the element should be shown, given the `loading` state. Defaults to `true`. */ 22 | matcher?: boolean | ((live: boolean) => boolean); 23 | } 24 | 25 | const LoadingIndicator = React.forwardRef< 26 | LoadingIndicatorElement, 27 | LoadingIndicatorProps 28 | >((props: MediaScopedProps , forwardedRef) => { 29 | const { 30 | __scopeMedia, 31 | forceMount, 32 | matcher = true, 33 | ...offlineErrorProps 34 | } = props; 35 | 36 | const context = useMediaContext(LOADING_INDICATOR_NAME, __scopeMedia); 37 | 38 | const loading = useStore(context.store, ({ loading }) => loading); 39 | 40 | const isPresent = useMemo( 41 | () => 42 | typeof matcher === "function" ? matcher(loading) : matcher === loading, 43 | [matcher, loading], 44 | ); 45 | 46 | return ( 47 | 48 | 57 | ); 58 | }); 59 | 60 | LoadingIndicator.displayName = LOADING_INDICATOR_NAME; 61 | 62 | export { LoadingIndicator }; 63 | export type { LoadingIndicatorProps }; 64 | -------------------------------------------------------------------------------- /packages/react/src/shared/Portal.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | // biome-ignore lint/style/useImportType: necessary import 4 | import React from "react"; 5 | 6 | import * as RadixPortal from "@radix-ui/react-portal"; 7 | 8 | const PORTAL_NAME = "Portal"; 9 | 10 | type PortalProps = React.ComponentPropsWithoutRef56 | ; 11 | 12 | const Portal: React.FC = (props: PortalProps) => { 13 | return ; 14 | }; 15 | 16 | Portal.displayName = PORTAL_NAME; 17 | 18 | export { Portal, type PortalProps }; 19 | -------------------------------------------------------------------------------- /packages/react/src/shared/Slider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as SliderPrimitive from "@radix-ui/react-slider"; 4 | 5 | type SliderProps = SliderPrimitive.SliderProps; 6 | const Root = SliderPrimitive.Root; 7 | type TrackProps = SliderPrimitive.SliderTrackProps; 8 | const Track = SliderPrimitive.Track; 9 | type RangeProps = SliderPrimitive.SliderRangeProps; 10 | const Range = SliderPrimitive.Range; 11 | type ThumbProps = SliderPrimitive.SliderThumbProps; 12 | const Thumb = SliderPrimitive.Thumb; 13 | 14 | export { Range, Root, Thumb, Track }; 15 | export type { RangeProps, SliderProps, ThumbProps, TrackProps }; 16 | -------------------------------------------------------------------------------- /packages/react/src/shared/Time.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React from "react"; 4 | 5 | import { useStore } from "zustand"; 6 | import { type MediaScopedProps, useMediaContext } from "./context"; 7 | 8 | import { useShallow } from "zustand/react/shallow"; 9 | import * as Radix from "./primitive"; 10 | 11 | const TIME_NAME = "Time"; 12 | 13 | type TimeElement = React.ElementRef ; 14 | 15 | interface TimeProps 16 | extends Omit< 17 | Radix.ComponentPropsWithoutRef , 18 | "children" 19 | > {} 20 | 21 | const Time = React.forwardRef ( 22 | (props: MediaScopedProps , forwardedRef) => { 23 | const { __scopeMedia, ...timeProps } = props; 24 | 25 | const context = useMediaContext(TIME_NAME, __scopeMedia); 26 | 27 | const { progress, duration, live, formattedTime } = useStore( 28 | context.store, 29 | useShallow(({ progress, duration, live, aria }) => ({ 30 | formattedTime: aria.time, 31 | progress, 32 | duration, 33 | live, 34 | })), 35 | ); 36 | 37 | return ( 38 | 48 | {formattedTime} 49 | 50 | ); 51 | }, 52 | ); 53 | 54 | Time.displayName = TIME_NAME; 55 | 56 | export { Time }; 57 | export type { TimeProps }; 58 | -------------------------------------------------------------------------------- /packages/react/src/shared/context.tsx: -------------------------------------------------------------------------------- 1 | import type { MediaControllerStore } from "@livepeer/core/media"; 2 | import { createContextScope } from "@radix-ui/react-context"; 3 | import { useStore as useStoreZustand } from "zustand"; 4 | 5 | import type { Scope } from "@radix-ui/react-context"; 6 | 7 | const MEDIA_NAME = "Media"; 8 | 9 | // biome-ignore lint/complexity/noBannedTypes: allow {} 10 | type MediaScopedProps= P & { __scopeMedia?: Scope }; 11 | const [createMediaContext, createMediaScope] = createContextScope(MEDIA_NAME); 12 | 13 | type MediaContextValue = { 14 | store: MediaControllerStore; 15 | }; 16 | 17 | const [MediaProvider, useMediaContext] = 18 | createMediaContext
(MEDIA_NAME); 19 | 20 | const useStore = useStoreZustand; 21 | 22 | export { MediaProvider, createMediaScope, useMediaContext, useStore }; 23 | export type { MediaContextValue, MediaScopedProps }; 24 | -------------------------------------------------------------------------------- /packages/react/src/shared/utils.ts: -------------------------------------------------------------------------------- 1 | export const noPropagate = 2 | < 3 | E extends { 4 | stopPropagation(): void; 5 | }, 6 | >( 7 | // biome-ignore lint/suspicious/noExplicitAny: any 8 | cb: (...args: any) => any, 9 | ) => 10 | (event: E) => { 11 | event.stopPropagation(); 12 | 13 | return cb(); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/react/test/index.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | type Queries, 3 | type RenderOptions, 4 | render as defaultRender, 5 | type queries, 6 | } from "@testing-library/react"; 7 | 8 | import type * as React from "react"; 9 | 10 | export const render = < 11 | Q extends Queries = typeof queries, 12 | Container extends Element | DocumentFragment = HTMLElement, 13 | BaseElement extends Element | DocumentFragment = Container, 14 | >( 15 | ui: React.ReactElement, 16 | options?: RenderOptions , 17 | ) => defaultRender(ui, { ...options }); 18 | 19 | export { act, cleanup, fireEvent, screen } from "@testing-library/react"; 20 | export { getSampleVideo } from "../../core/test"; 21 | -------------------------------------------------------------------------------- /packages/react/test/setup.ts: -------------------------------------------------------------------------------- 1 | import { vi } from "vitest"; 2 | 3 | // make dates stable across runs 4 | Date.now = vi.fn(() => new Date(Date.UTC(2022, 1, 1)).valueOf()); 5 | 6 | type ReactVersion = "17" | "18"; 7 | const reactVersion: ReactVersion = 8 |process.env.REACT_VERSION || "18"; 9 | 10 | // set up imports for React 17 11 | vi.mock("@testing-library/react-hooks", async () => { 12 | const packages = { 13 | "18": "@testing-library/react", 14 | "17": "@testing-library/react-hooks", 15 | }; 16 | 17 | return await vi.importActual(packages[reactVersion]); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react", 4 | "moduleResolution": "bundler", 5 | "esModuleInterop": true, 6 | "target": "ESNext", 7 | "lib": ["es2015", "dom"], 8 | "strict": true, 9 | "strictNullChecks": true 10 | }, 11 | "watchOptions": { 12 | "watchFile": "useFsEvents", 13 | "watchDirectory": "useFsEvents", 14 | "fallbackPolling": "dynamicPriority" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/react/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "tsup"; 2 | 3 | /** @type {import('tsup').Options} */ 4 | const options = { 5 | splitting: false, 6 | clean: true, 7 | sourcemap: true, 8 | dts: true, 9 | format: ["esm", "cjs"], 10 | }; 11 | 12 | /** @type {import('tsup').Options} */ 13 | const reactServerOptions = { 14 | ...options, 15 | external: ["react"], 16 | }; 17 | 18 | /** @type {import('tsup').Options} */ 19 | const reactClientOptions = { 20 | ...reactServerOptions, 21 | esbuildOptions: (options) => { 22 | // Append "use client" to the top of the react entry point 23 | options.banner = { 24 | js: '"use client";', 25 | }; 26 | }, 27 | }; 28 | 29 | const entrypoints = ["crypto", "external"]; 30 | const reactServerEntrypoints = ["assets"]; 31 | const reactClientEntrypoints = ["broadcast", "player"]; 32 | 33 | export default defineConfig([ 34 | { 35 | ...options, 36 | entry: { 37 | index: "src/index.ts", 38 | }, 39 | outDir: "dist", 40 | }, 41 | ...entrypoints.map((entrypoint) => ({ 42 | ...options, 43 | entry: { 44 | index: `src/${entrypoint}.ts`, 45 | }, 46 | outDir: `dist/${entrypoint}`, 47 | })), 48 | ...reactServerEntrypoints.map((reactEntrypoint) => ({ 49 | ...reactServerOptions, 50 | entry: { 51 | index: `src/${reactEntrypoint}.tsx`, 52 | }, 53 | outDir: `dist/${reactEntrypoint}`, 54 | })), 55 | ...reactClientEntrypoints.map((reactEntrypoint) => ({ 56 | ...reactClientOptions, 57 | entry: { 58 | index: `src/${reactEntrypoint}.tsx`, 59 | }, 60 | outDir: `dist/${reactEntrypoint}`, 61 | })), 62 | ]); 63 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "apps/*" 3 | - "examples/*" 4 | - "packages/*" 5 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "allowJs": true, 4 | "downlevelIteration": true, 5 | "esModuleInterop": true, 6 | "incremental": true, 7 | "isolatedModules": true, 8 | "jsx": "react-jsx", 9 | "lib": ["es2019", "es2017", "dom"], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "noEmit": true, 13 | "noImplicitAny": true, 14 | "noUncheckedIndexedAccess": true, 15 | "noUnusedLocals": true, 16 | "noUnusedParameters": true, 17 | "resolveJsonModule": true, 18 | "skipLibCheck": true, 19 | "strict": true, 20 | "strictNullChecks": true, 21 | "target": "es2021", 22 | "types": ["node"] 23 | }, 24 | "exclude": ["node_modules", "**/dist/**"], 25 | "include": ["packages/**/*"], 26 | "watchOptions": { 27 | "watchFile": "useFsEvents", 28 | "watchDirectory": "useFsEvents", 29 | "fallbackPolling": "dynamicPriority" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "globalDependencies": ["**/.env"], 4 | "globalEnv": ["NODE_ENV"], 5 | "pipeline": { 6 | "build": { 7 | "dependsOn": ["generate", "^build"], 8 | "env": [], 9 | "outputs": ["dist/**", ".next/**"] 10 | }, 11 | "dev": { 12 | "cache": false, 13 | "persistent": true 14 | }, 15 | "generate": { 16 | "cache": false 17 | }, 18 | "test": { 19 | "cache": false 20 | }, 21 | "lint": {}, 22 | "clean": { 23 | "cache": false 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { configDefaults, defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | coverage: { 6 | reporter: ["text", "json", "html"], 7 | exclude: [ 8 | ...configDefaults.exclude, 9 | "**/examples/**", 10 | "**/test/**", 11 | "**/*.d.ts", 12 | "**/generate-version.ts", 13 | ], 14 | }, 15 | environment: "jsdom", 16 | setupFiles: [ 17 | "./packages/core/test/setup.ts", 18 | "./packages/react/test/setup.ts", 19 | ], 20 | }, 21 | }); 22 | --------------------------------------------------------------------------------