diff --git a/README.md b/README.md
index 450adcd15..3d2ead0b7 100644
--- a/README.md
+++ b/README.md
@@ -178,7 +178,7 @@ If you want to import the player as a React component into your own code, or use
parts of the player, you'll need to install the package as a dependency.
You will also need to install a number of peer dependencies
such as [luxon](https://github.com/moment/luxon), which we use for date and time purposes,
-`react`/`react-dom`, `styled-components`.
+`react`/`react-dom`.
You can find an example of this under `example-player-react`, e.g.:
```js
diff --git a/example-overlay-react/App.jsx b/example-overlay-react/App.jsx
index b252de255..c660a4565 100644
--- a/example-overlay-react/App.jsx
+++ b/example-overlay-react/App.jsx
@@ -1,7 +1,5 @@
import React, { useState } from 'react'
-import styled from 'styled-components'
-
import {
Foundation,
Liner,
@@ -25,13 +23,6 @@ const MIDDLE_AREA = [
[0.5, -0.5], // bottom right coordinate
]
-const Layers = styled.div`
- position: relative;
- width: 80vw;
- height: 80vh;
- border: 1px solid deepskyblue;
-`
-
const App = () => {
const [textPos1, setTextPos1] = useState([-1, 0.8])
const [textPos2, setTextPos2] = useState([-0.4, -0.5])
@@ -54,7 +45,12 @@ const App = () => {
To get started, edit src/App.tsx
and save to reload.
-
+
{
interact with the SVGs below me
-
+
)
}
diff --git a/example-overlay-react/Circle.jsx b/example-overlay-react/Circle.jsx
index 32e1ac740..e37081dda 100644
--- a/example-overlay-react/Circle.jsx
+++ b/example-overlay-react/Circle.jsx
@@ -6,19 +6,12 @@ import React, {
useState,
} from 'react'
-import styled from 'styled-components'
-
import {
FoundationContext,
LinerContext,
useDraggable,
} from 'media-stream-library/overlay'
-const SvgCircle = styled.circle`
- fill: rgb(0.5, 0.5, 0.5, 0.2);
- stroke: grey;
-`
-
/*
* Circle
*
@@ -36,7 +29,7 @@ export const Circle = forwardRef(
const [cx, cy] = toSvgBasis(pos)
- return
+ return
}
)
@@ -99,7 +92,8 @@ export const DraggableCircle = forwardRef(({ pos, onChangePos, ...circleProps },
const [cx, cy] = svgPos
return (
- {
const { toSvgBasis, toUserBasis } = useContext(FoundationContext)
const { clampCoord, clampCoordArray } = useContext(LinerContext)
@@ -86,15 +66,16 @@ export const Polygon = ({ pos, onChangePos }) => {
return (
- `${x},${y}`).join(' ')} />
+ `${x},${y}`).join(' ')} />
{svgPos.map(([x, y], index) => {
// The visible corners
- return
+ return
})}
{svgPos.map(([x, y], index) => {
// The invisible handles
return (
-
{children}
-
+
)
}
diff --git a/example-overlay-react/package.json b/example-overlay-react/package.json
index 39b0c03e4..720a81397 100644
--- a/example-overlay-react/package.json
+++ b/example-overlay-react/package.json
@@ -2,16 +2,12 @@
"private": true,
"dependencies": {
"media-stream-library": "workspace:^",
- "pepjs": "0.5.3",
- "react": "18.3.1",
- "react-dom": "18.3.1",
- "styled-components": "5.3.11"
+ "pepjs": "^0.5.3",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1"
},
"devDependencies": {
- "@types/react": "18.3.18",
- "@types/react-dom": "18.3.5",
- "@types/styled-components": "5.1.34",
- "@vitejs/plugin-react": "4.3.4",
- "vite": "6.0.7"
+ "@vitejs/plugin-react": "^4.3.4",
+ "vite": "^6.0.7"
}
}
diff --git a/example-player-react/App.jsx b/example-player-react/App.jsx
index e329047a0..a733916cf 100644
--- a/example-player-react/App.jsx
+++ b/example-player-react/App.jsx
@@ -1,38 +1,34 @@
import React, { useCallback, useState } from 'react'
-import styled, { createGlobalStyle } from 'styled-components'
-
import { BasicStream } from './BasicStream'
import { MultiStream } from './MultiStream'
import { SingleStream } from './SingleStream'
-const GlobalStyle = createGlobalStyle`
- body {
- margin: 0;
- font-family: sans-serif;
- }
-`
-
-const AppContainer = styled.div`
+const style = `
+body {
+ margin: 0;
+ font-family: sans-serif;
+}
+.appContainer {
width: 100vw;
height: 90vh;
display: flex;
flex-direction: column;
align-items: center;
-`
-
-const ButtonContainer = styled.div`
+}
+.buttonContainer {
margin: 8px;
text-align: center;
+ & button {
+ padding: 8px 12px;
+ margin: 4px;
+ background-color: lightpink;
+ &.selected {
+ background-color: lightgreen;
+ }
+ }
+}
`
-
-const Button = styled.button`
- padding: 8px 12px;
- margin: 4px;
- background-color: ${({ selected }) =>
- selected ? 'lightgreen' : 'lightpink'};
-`
-
const LOCALSTORAGE_KEY = 'media-stream-player-example'
export const App = () => {
@@ -55,23 +51,25 @@ export const App = () => {
}, [setState])
return (
-
-
- Media Stream Player
-
-
- Single stream (with controls)
-
-
- Single stream (basic)
-
-
- Multi stream
-
-
- {state === 'single' ? : null}
- {state === 'basic' ? : null}
- {state === 'multi' ? : null}
-
+ <>
+
+
+
Media Stream Player
+
+
+ Single stream (with controls)
+
+
+ Single stream (basic)
+
+
+ Multi stream
+
+
+ {state === 'single' ?
: null}
+ {state === 'basic' ?
: null}
+ {state === 'multi' ?
: null}
+
+ >
)
}
diff --git a/example-player-react/MultiStream.jsx b/example-player-react/MultiStream.jsx
index e10770c13..a4860ec55 100644
--- a/example-player-react/MultiStream.jsx
+++ b/example-player-react/MultiStream.jsx
@@ -1,25 +1,7 @@
import React, { useEffect, useState } from 'react'
-import styled from 'styled-components'
-
import { Player } from 'media-stream-library/player'
-const MediaPlayer = styled(Player)`
- max-width: 400px;
- max-height: 300px;
- margin: 8px;
-`
-
-const MediaPlayerContainer = styled.div`
- width: 400px;
- height: 300px;
- margin: 8px;
-`
-
-const Centered = styled.div`
- text-align: center;
-`
-
// force auth
const authorize = async (host) => {
// Force a login by fetching usergroup
@@ -73,21 +55,22 @@ export const MultiStream = () => {
{state.length > 0 ? (
state.map((device) => {
return device.authorized ? (
-
- {device.hostname}
-
+ {device.hostname}
+
-
+
) : (
-
+
{device.hostname}
Not authorized
-
+
)
})
) : (
diff --git a/example-player-react/package.json b/example-player-react/package.json
index 9ccd78d41..41a6a9d81 100644
--- a/example-player-react/package.json
+++ b/example-player-react/package.json
@@ -1,12 +1,11 @@
{
"private": true,
"devDependencies": {
- "@vitejs/plugin-react": "4.3.4",
- "luxon": "3.5.0",
+ "@vitejs/plugin-react": "^4.3.4",
+ "luxon": "^3.5.0",
"media-stream-library": "workspace:^",
- "react": "18.3.1",
- "react-dom": "18.3.1",
- "styled-components": "5.3.11",
- "vite": "6.0.7"
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "vite": "^6.0.7"
}
}
diff --git a/package.json b/package.json
index d69523d23..b952d430d 100644
--- a/package.json
+++ b/package.json
@@ -40,8 +40,7 @@
"luxon": "^3.0.0",
"pepjs": ">= 0.5.3 < 1",
"react": "^17.0.1 || ^18.0.0",
- "react-dom": "^17.0.1 || ^18.0.0",
- "styled-components": "^5.3.5"
+ "react-dom": "^17.0.1 || ^18.0.0"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
@@ -51,7 +50,6 @@
"@types/node": "22.10.5",
"@types/react": "18.3.18",
"@types/react-dom": "18.3.5",
- "@types/styled-components": "5.1.34",
"@types/ws": "8.5.13",
"@vitejs/plugin-react": "4.3.4",
"c8": "10.1.3",
@@ -67,7 +65,6 @@
"react-dom": "18.3.1",
"react-is": "18.3.1",
"semver": "7.6.3",
- "styled-components": "5.3.11",
"typescript": "5.7.3",
"uvu": "0.5.6",
"vite": "6.0.7",
diff --git a/src/overlay/Foundation.tsx b/src/overlay/Foundation.tsx
index 72b05be07..145c4a0e9 100644
--- a/src/overlay/Foundation.tsx
+++ b/src/overlay/Foundation.tsx
@@ -12,28 +12,12 @@ import React, {
useState,
} from 'react'
-import styled, { css } from 'styled-components'
-
import { Matrix, apply, inverse, multiply } from './utils/affine'
import { Coord } from './utils/geometry'
type BaseElement = HTMLDivElement
type BaseProps = HTMLAttributes
-const Container = styled.div<{ readonly clickThrough: boolean }>`
- ${({ clickThrough }) =>
- clickThrough
- ? css`
- pointer-events: none;
- & > svg > * {
- pointer-events: initial;
- }
- `
- : css`
- pointer-events: initial;
- `}
-`
-
// Prototype of an Svg implementation with basis transform capabilities.
/**
@@ -212,7 +196,7 @@ export const Foundation = forwardRef<
userBasis = DEFAULT_USER_BASIS,
transformationMatrix,
onReady,
- className,
+ className = '',
clickThrough = false,
children,
...externalProps
@@ -341,24 +325,28 @@ export const Foundation = forwardRef<
[userBasis, toSvgBasis, toUserBasis]
)
+ const containerClassName = `${className} ${clickThrough ? 'clickthrough' : ''}`
+
/**
* Render SVG drawing area.
*/
return (
-
-
- {contextValue !== undefined ? (
-
- {children}
-
- ) : null}
-
-
+ <>
+
+
+
+ {contextValue !== undefined ? (
+
+ {children}
+
+ ) : null}
+
+
+ >
)
}
)
diff --git a/src/player/BasicPlayer.tsx b/src/player/BasicPlayer.tsx
index c4ea9d00f..c3127df09 100644
--- a/src/player/BasicPlayer.tsx
+++ b/src/player/BasicPlayer.tsx
@@ -9,22 +9,39 @@ import React, {
} from 'react'
import { Container, Layer } from './Container'
-import { ControlArea, ControlBar } from './Controls'
import {
PlaybackArea,
PlayerNativeElement,
VapixParameters,
VideoProperties,
} from './PlaybackArea'
-import { Button } from './components/Button'
+import { Pause, Play } from './components'
import { Limiter } from './components/Limiter'
-import { MediaStreamPlayerContainer } from './components/MediaStreamPlayerContainer'
import { useUserActive } from './hooks/useUserActive'
-import { Pause, Play } from './img'
import { Format } from './types'
const DEFAULT_FORMAT = Format.JPEG
+const controlAreaStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ fontFamily: 'sans',
+ height: '100%',
+ justifyContent: 'flex-end',
+ transition: 'opacity 0.3s ease-in-out',
+ width: '100%',
+} as const
+
+const controlBarStyle = {
+ width: '100%',
+ height: '32px',
+ background: 'rgb(0, 0, 0, 0.66)',
+ display: 'flex',
+ alignItems: 'center',
+ padding: '0 16px',
+ boxSizing: 'border-box',
+} as const
+
interface BasicPlayerProps {
readonly hostname: string
readonly vapixParams?: VapixParameters
@@ -146,8 +163,13 @@ export const BasicPlayer = forwardRef(
* container size.
*/
+ const visible = play !== true || userActive
+
return (
-
+
@@ -164,24 +186,22 @@ export const BasicPlayer = forwardRef(
/>
-
-
-
- {play === true ? (
-
- ) : (
-
- )}
-
-
-
+
+ {play === true ? (
+
+ ) : (
+
+ )}
+
+
-
+
)
}
)
diff --git a/src/player/Container.tsx b/src/player/Container.tsx
index 52422600d..992ecfcdb 100644
--- a/src/player/Container.tsx
+++ b/src/player/Container.tsx
@@ -1,6 +1,4 @@
-import React from 'react'
-
-import styled from 'styled-components'
+import React, { PropsWithChildren } from 'react'
/**
* Aspect ratio
@@ -33,30 +31,38 @@ const getHeightPct = (aspectRatio: number) => {
return 100 / aspectRatio
}
-const ContainerBody = styled.div.attrs<{ readonly aspectRatio: number }>(
- ({ aspectRatio }) => {
- return { style: { paddingTop: `${getHeightPct(aspectRatio)}%` } }
- }
-)<{ readonly aspectRatio: number }>`
- width: 100%;
- background: black;
- position: relative;
-`
-
-export const Layer = styled.div`
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- right: 0;
-`
+export const Layer = ({ children }: PropsWithChildren) => {
+ return (
+
+ {children}
+
+ )
+}
interface ContainerProps {
readonly aspectRatio?: number
- readonly children: any // styled-components type mismatch
}
-export const Container: React.FC = ({
+export const Container = ({
aspectRatio = DEFAULT_ASPECT_RATIO,
children,
-}) => {children}
+}: PropsWithChildren) => (
+
+ {children}
+
+)
diff --git a/src/player/Controls.tsx b/src/player/Controls.tsx
index 8d81a1f98..dc8dcb119 100644
--- a/src/player/Controls.tsx
+++ b/src/player/Controls.tsx
@@ -1,126 +1,105 @@
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { DateTime, Duration } from 'luxon'
-import styled from 'styled-components'
import { VapixParameters, VideoProperties } from './PlaybackArea'
import { Settings } from './Settings'
-import { Button } from './components/Button'
+import { CogWheel, Pause, Play, Refresh, Screenshot, Stop } from './components'
import { useUserActive } from './hooks/useUserActive'
-import { CogWheel, Pause, Play, Refresh, Screenshot, Stop } from './img'
import { Format } from './types'
function isHTMLMediaElement(el: HTMLElement): el is HTMLMediaElement {
return (el as HTMLMediaElement).buffered !== undefined
}
-export const ControlArea = styled.div<{ readonly visible: boolean }>`
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- justify-content: flex-end;
- opacity: ${({ visible }) => (visible ? 1 : 0)};
- transition: opacity 0.3s ease-in-out;
-`
-
-export const ControlBar = styled.div`
- width: 100%;
- height: 32px;
- background: rgb(0, 0, 0, 0.66);
- display: flex;
- align-items: center;
- padding: 0 16px;
- box-sizing: border-box;
-`
-
-const VolumeContainer = styled.div`
- margin-left: 8px;
-`
-
-const Progress = styled.div`
- flex-grow: 2;
- padding: 0 32px;
- display: flex;
- align-items: center;
-`
-
-const ProgressBarContainer = styled.div`
- margin: 0;
- width: 100%;
- height: 24px;
- position: relative;
- display: flex;
- flex-direction: column;
- justify-content: center;
-`
-
-const ProgressBar = styled.div`
- background-color: rgba(255, 255, 255, 0.1);
- height: 1px;
- position: relative;
- width: 100%;
-
- ${ProgressBarContainer}:hover > & {
- height: 3px;
- }
-`
+const controlAreaStyle = {
+ display: 'flex',
+ flexDirection: 'column',
+ fontFamily: 'sans',
+ height: '100%',
+ justifyContent: 'flex-end',
+ transition: 'opacity 0.3s ease-in-out',
+ width: '100%',
+} as const
+
+const controlBarStyle = {
+ width: '100%',
+ height: '32px',
+ background: 'rgb(0, 0, 0, 0.66)',
+ display: 'flex',
+ alignItems: 'center',
+ padding: '0 16px',
+ boxSizing: 'border-box',
+} as const
+
+const progressStyle = {
+ flexGrow: '2',
+ padding: '0 32px',
+ display: 'flex',
+ alignItems: 'center',
+} as const
+
+const progressBarContainerStyle = {
+ margin: '0',
+ width: '100%',
+ height: '24px',
+ position: 'relative',
+ display: 'flex',
+ flexDirection: 'column',
+ justifyContent: 'center',
+} as const
+
+const progressBarStyle = {
+ backgroundColor: 'rgba(255, 255, 255, 0.1)',
+ height: '1px',
+ position: 'relative',
+ width: '100%',
+} as const
+
+const progressBarPlayedStyle = (fraction = 0) => {
+ return {
+ transform: `scaleX(${fraction})`,
+ backgroundColor: 'rgb(240, 180, 0)',
+ height: '100%',
+ position: 'absolute',
+ top: '0',
+ transformOrigin: '0 0',
+ width: '100%',
+ } as const
+}
-const ProgressBarPlayed = styled.div.attrs<{ readonly fraction: number }>(
- ({ fraction }) => {
- return {
- style: { transform: `scaleX(${fraction})` },
- }
- }
-)<{ readonly fraction: number }>`
- background-color: rgb(240, 180, 0);
- height: 100%;
- position: absolute;
- top: 0;
- transform: scaleX(0);
- transform-origin: 0 0;
- width: 100%;
-`
-
-const ProgressBarBuffered = styled.div.attrs<{ readonly fraction: number }>(
- ({ fraction }) => {
- return {
- style: { transform: `scaleX(${fraction})` },
- }
- }
-)<{ readonly fraction: number }>`
- background-color: rgba(255, 255, 255, 0.2);
- height: 100%;
- position: absolute;
- top: 0;
- transform: scaleX(0);
- transform-origin: 0 0;
- width: 100%;
-`
-
-const ProgressTimestamp = styled.div.attrs<{ readonly left: number }>(
- ({ left }) => {
- return {
- style: { left: `${left}px` },
- }
- }
-)<{ readonly left: number }>`
- background-color: rgb(56, 55, 51);
- border-radius: 3px;
- bottom: 200%;
- color: #fff;
- font-size: 9px;
- padding: 5px;
- position: absolute;
- text-align: center;
-`
-
-const ProgressIndicator = styled.div`
- color: rgb(240, 180, 0);
- padding-left: 24px;
- font-size: 10px;
- white-space: nowrap;
-`
+const progressBarBufferedStyle = (fraction = 0) => {
+ return {
+ transform: `scaleX(${fraction})`,
+ backgroundColor: 'rgba(255, 255, 255, 0.2)',
+ height: '100%',
+ position: 'absolute',
+ top: '0',
+ transformOrigin: '0 0',
+ width: '100%',
+ } as const
+}
+
+const progressBarTimestampStyle = (left = 0) => {
+ return {
+ left: `${left}px`,
+ backgroundColor: 'rgb(56, 55, 51)',
+ borderRadius: '3px',
+ bottom: '200%',
+ color: '#fff',
+ fontSize: '9px',
+ padding: '5px',
+ position: 'absolute',
+ textAlign: 'center',
+ } as const
+}
+
+const progressIndicatorStyle = {
+ color: 'rgb(240, 180, 0)',
+ paddingLeft: '24px',
+ fontSize: '10px',
+ whiteSpace: 'nowrap',
+} as const
interface ControlsProps {
readonly play?: boolean
@@ -344,36 +323,28 @@ export const Controls: React.FC = ({
}
}, [startTime, totalDuration])
+ const visible = play !== true || settings || userActive
+
return (
-
-
-
- {play === true ? (
-
- ) : (
-
- )}
-
- {src !== undefined && (
-
-
-
+
+ {play === true ? (
+
+ ) : (
+
)}
+ {src !== undefined &&
}
{src !== undefined && (
-
-
-
+
)}
{src !== undefined && (
-
-
-
+
)}
{volume !== undefined ? (
-
+
= ({
onChange={onVolumeChange}
value={volume ?? 0}
/>
-
+
) : null}
-
-
-
-
-
+
+
+
+
+
{timestamp.left !== 0 ? (
-
+
{timestamp.label}
-
+
) : null}
-
-
-
+
+
+
{totalDuration === Infinity ? '∙ LIVE' : progress.counter}
-
-
-
-
-
-
+
+
+
+
{settings && (
= ({
toggleStats={toggleStats}
/>
)}
-
+
)
}
diff --git a/src/player/Feedback.tsx b/src/player/Feedback.tsx
index d8d01a57a..5cb24a3b8 100644
--- a/src/player/Feedback.tsx
+++ b/src/player/Feedback.tsx
@@ -1,22 +1,22 @@
import React from 'react'
-import styled from 'styled-components'
-
-import { Spinner } from './img'
-
-const FeedbackArea = styled.div`
- width: 100%;
- height: 100%;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
-`
+import { Spinner } from './components'
interface FeedbackProps {
readonly waiting?: boolean
}
export const Feedback: React.FC = ({ waiting = false }) => (
- {waiting && }
+
+ {waiting && }
+
)
diff --git a/src/player/HttpMp4Video.tsx b/src/player/HttpMp4Video.tsx
index 9f6b9b3a4..6e13852a3 100644
--- a/src/player/HttpMp4Video.tsx
+++ b/src/player/HttpMp4Video.tsx
@@ -1,22 +1,14 @@
import React, { useEffect, useRef, useState } from 'react'
-import styled from 'styled-components'
import { HttpMp4Pipeline, TransformationMatrix } from '../streams'
import { VideoProperties } from './PlaybackArea'
import { FORMAT_SUPPORTS_AUDIO } from './constants'
import { useEventState } from './hooks/useEventState'
-import { useVideoDebug } from './hooks/useVideoDebug'
import { MetadataHandler } from './metadata'
import { Format } from './types'
import { logDebug } from './utils/log'
-const VideoNative = styled.video`
- max-height: 100%;
- object-fit: contain;
- width: 100%;
-`
-
/**
* WebSocket + RTSP playback component.
*/
@@ -91,8 +83,7 @@ export const HttpMp4Video: React.FC = ({
__onEndedRef.current = onEnded
const __sensorTmRef = useRef()
-
- useVideoDebug(videoRef.current)
+ const __mimeRef = useRef('video/mp4')
useEffect(() => {
const videoEl = videoRef.current
@@ -117,12 +108,13 @@ export const HttpMp4Video: React.FC = ({
if (__onPlayingRef.current !== undefined) {
__onPlayingRef.current({
el: videoEl,
- width: videoEl.videoWidth,
+ formatSupportsAudio: FORMAT_SUPPORTS_AUDIO[Format.MP4_H264],
height: videoEl.videoHeight,
+ mime: __mimeRef.current,
+ pipeline: pipeline ?? undefined,
sensorTm: __sensorTmRef.current,
- formatSupportsAudio: FORMAT_SUPPORTS_AUDIO[Format.MP4_H264],
- // TODO: no volume, need to expose tracks?
- // TODO: no pipeline, can we even get stats?
+ volume: videoEl.volume,
+ width: videoEl.videoWidth,
})
}
}
@@ -160,21 +152,34 @@ export const HttpMp4Video: React.FC = ({
const endedCallback = () => {
__onEndedRef.current?.()
}
- pipeline.start().then(({ headers, finished }) => {
- __sensorTmRef.current = parseTransformHeader(
- headers.get('video-sensor-transform') ??
- headers.get('video-metadata-transform')
- )
- finished.finally(() => {
- endedCallback()
+ pipeline
+ .start()
+ .then(({ headers, finished }) => {
+ __mimeRef.current = headers.get('content-type') ?? 'video/mp4'
+ __sensorTmRef.current = parseTransformHeader(
+ headers.get('video-sensor-transform') ??
+ headers.get('video-metadata-transform')
+ )
+ finished.finally(() => {
+ endedCallback()
+ })
+ })
+ .catch((err) => {
+ console.error('failed to fetch video stream:', err)
})
- })
logDebug('initiated data fetching')
setFetching(true)
}
}, [play, pipeline, fetching])
- return
+ return (
+
+ )
}
const parseTransformHeader = (
diff --git a/src/player/PlaybackArea.tsx b/src/player/PlaybackArea.tsx
index 096594010..aa345450b 100644
--- a/src/player/PlaybackArea.tsx
+++ b/src/player/PlaybackArea.tsx
@@ -63,9 +63,10 @@ export interface VideoProperties {
readonly formatSupportsAudio: boolean
readonly pipeline?: PlayerPipeline
readonly media?: ReadonlyArray<{
- readonly type: 'video' | 'audio' | 'data'
- readonly mime: string
+ readonly codec?: string
+ readonly name?: string
}>
+ readonly mime?: string
readonly volume?: number
readonly range?: Range
readonly sensorTm?: TransformationMatrix
@@ -93,7 +94,9 @@ interface PlaybackAreaProps {
const wsUri = (secure: boolean, host: string) => {
const scheme = secure ? Protocol.HTTPS : Protocol.HTTP
- return axisWebSocketConfig(`${scheme}//${host}`)
+ return host.length !== 0
+ ? axisWebSocketConfig(`${scheme}//${host}`)
+ : { uri: '', tokenUri: '' }
}
const rtspUri = (host: string, searchParams: string) => {
@@ -387,7 +390,7 @@ export const PlaybackArea: React.FC = ({
}
- {...{ src, play, onPlaying, onEnded }}
+ {...{ src, play, onPlaying }}
/>
)
}
diff --git a/src/player/Player.tsx b/src/player/Player.tsx
index cfc61076a..0d0afd312 100644
--- a/src/player/Player.tsx
+++ b/src/player/Player.tsx
@@ -21,7 +21,6 @@ import {
} from './PlaybackArea'
import { Stats } from './Stats'
import { Limiter } from './components/Limiter'
-import { MediaStreamPlayerContainer } from './components/MediaStreamPlayerContainer'
import { useSwitch } from './hooks/useSwitch'
import { MetadataHandler } from './metadata'
import { Format } from './types'
@@ -110,7 +109,6 @@ export const Player = forwardRef(
? window.localStorage.getItem('stats-overlay') === 'on'
: false
)
- const [statsExpanded, setStatsExpanded] = useState(true)
useEffect(() => {
if (window?.localStorage !== undefined) {
@@ -276,7 +274,10 @@ export const Player = forwardRef(
*/
return (
-
+
@@ -336,13 +337,11 @@ export const Player = forwardRef(
videoProperties={videoProperties}
refresh={refresh}
volume={volume}
- expanded={statsExpanded}
- onToggleExpanded={setStatsExpanded}
/>
) : null}
-
+
)
}
)
diff --git a/src/player/Settings.tsx b/src/player/Settings.tsx
index 77231235c..a19e89fbf 100644
--- a/src/player/Settings.tsx
+++ b/src/player/Settings.tsx
@@ -1,46 +1,8 @@
import React, { ChangeEventHandler, useCallback, useRef, useState } from 'react'
-import styled from 'styled-components'
-
import { VapixParameters } from './PlaybackArea'
-import { Switch } from './components/Switch'
import { Format } from './types'
-const SettingsMenu = styled.div`
- font-family: sans-serif;
- display: flex;
- flex-direction: column;
- position: absolute;
- bottom: 32px;
- right: 0;
- background: rgb(0, 0, 0, 0.66);
- padding: 8px 16px;
- margin-bottom: 16px;
- margin-right: 8px;
-
- &:after {
- content: '';
- width: 10px;
- height: 10px;
- transform: rotate(45deg);
- position: absolute;
- bottom: -5px;
- right: 12px;
- background: rgb(0, 0, 0, 0.66);
- }
-`
-
-const SettingsItem = styled.div`
- display: flex;
- flex-direction: row;
- color: white;
- height: 24px;
- width: 320px;
- align-items: center;
- justify-content: space-between;
- margin: 4px 0;
-`
-
interface SettingsProps {
readonly parameters: VapixParameters
readonly format: Format
@@ -112,64 +74,85 @@ export const Settings: React.FC = ({
)
return (
-
-
- Format
-
- H.264 (RTP over WS)
- H.264 (MP4 over HTTP)
- Motion JPEG
- Still image
-
-
-
- Resolution
-
- default
- 1920 x 1080 (FHD)
- 1280 x 720 (HD)
- 800 x 600 (VGA)
-
-
-
- Rotation
-
- 0
- 90
- 180
- 270
-
-
-
- Compression
-
- default
- 0
- 10
- 20
- 30
- 40
- 50
- 60
- 70
- 80
- 90
- 100
-
-
-
- Text overlay
-
-
+ Format
+
+ H.264 (RTP over WS)
+ H.264 (MP4 over HTTP)
+ Motion JPEG
+ Still image
+
+ Resolution
+
+ default
+ 1920 x 1080 (FHD)
+ 1280 x 720 (HD)
+ 800 x 600 (VGA)
+
+ Rotation
+
+ 0
+ 90
+ 180
+ 270
+
+ Compression
+
+ default
+ 0
+ 10
+ 20
+ 30
+ 40
+ 50
+ 60
+ 70
+ 80
+ 90
+ 100
+
+ Text overlay
+
+ Stats overlay
+
+
)
}
diff --git a/src/player/Stats.tsx b/src/player/Stats.tsx
index e337ae61c..7772faa3e 100644
--- a/src/player/Stats.tsx
+++ b/src/player/Stats.tsx
@@ -1,270 +1,128 @@
-import React, {
- MouseEventHandler,
- useCallback,
- useEffect,
- useState,
-} from 'react'
-import styled from 'styled-components'
+import React, { useCallback, useEffect, useState } from 'react'
-import { RtspMp4Pipeline } from '../streams'
+import { HttpMp4Pipeline, RtspJpegPipeline, RtspMp4Pipeline } from '../streams'
import { PlayerPipeline, VideoProperties } from './PlaybackArea'
-import { useInterval } from './hooks/useInterval'
-import { StreamStats } from './img'
import { Format } from './types'
-const isRtspMp4Pipeline = (
- pipeline: PlayerPipeline | undefined
-): pipeline is RtspMp4Pipeline => {
- return (pipeline as RtspMp4Pipeline)?.mp4.tracks !== undefined
+function isRtspMp4Pipeline(
+ pipeline: PlayerPipeline
+): pipeline is RtspMp4Pipeline {
+ return 'mp4' in pipeline
}
-const StatsWrapper = styled.div`
- position: absolute;
- top: 24px;
- left: 24px;
- width: 360px;
- min-width: 240px;
- max-width: 80%;
- max-height: 80%;
- border-radius: 4px;
- background: #292929 0% 0% no-repeat padding-box;
- opacity: 0.88;
-`
-
-const StatsHeader = styled.div`
- padding: 8px 24px;
- border-bottom: 1px solid #525252;
-`
-
-const StatsIcon = styled.span<{ readonly clickable: boolean }>`
- width: 24px;
- height: 24px;
- float: left;
- cursor: ${({ clickable }) => (clickable ? 'pointer' : 'default')};
-
- & > svg {
- fill: #e0e0e0;
- }
-`
-
-const StatsTitle = styled.span`
- display: inline-block;
- margin-left: 8px;
- vertical-align: sub;
- text-align: left;
- font-size: 16px;
- font-family: 'Open Sans', Sans-Serif;
- line-height: 22px;
- color: #f5f5f5;
-`
-
-const StatsHide = styled.span`
- float: right;
- text-align: right;
-`
-
-const HideLink = styled.a`
- vertical-align: sub;
- text-decoration: none;
- font-size: 16px;
- font-family: 'Open Sans', Sans-Serif;
- line-height: 22px;
- color: #b8b8b8;
-`
-
-const Data = styled.div`
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- column-gap: 24px;
- row-gap: 16px;
- width: 100%;
- padding: 16px 24px 24px;
-`
-
-const StatItem = styled.div`
- text-align: left;
- font-family: 'Open Sans', Sans-Serif;
-`
-
-const StatName = styled.div`
- font-size: 12px;
- line-height: 17px;
- color: #b8b8b8;
-`
-
-const StatValue = styled.div`
- font-size: 13px;
- line-height: 18px;
- color: #e0e0e0;
-`
+function isRtspJpegPipeline(
+ pipeline: PlayerPipeline
+): pipeline is RtspJpegPipeline {
+ return 'canvas' in pipeline
+}
-const StatsShow = styled.div`
- position: absolute;
- top: 24px;
- left: 24px;
- width: 32px;
- height: 32px;
- display: flex;
- justify-content: center;
- align-items: center;
- border-radius: 4px;
- background: #292929 0% 0% no-repeat padding-box;
- opacity: 0.88;
-`
+function isHttpMp4Pipeline(
+ pipeline: PlayerPipeline
+): pipeline is HttpMp4Pipeline {
+ return 'mediaElement' in pipeline
+}
interface StatsProps {
- readonly format: string
+ readonly format: Format
readonly videoProperties: VideoProperties
readonly refresh: number
readonly volume?: number
- readonly expanded: boolean
- readonly onToggleExpanded: (value: boolean) => void
}
-interface Stat {
- readonly name: string
- readonly value: string | number
- readonly unit?: string
+const streamLabel: Record = {
+ [Format.JPEG]: 'HTTP',
+ [Format.MJPEG]: 'HTTP',
+ [Format.RTP_H264]: 'WebSocket+RTSP',
+ [Format.RTP_JPEG]: 'WebSocket+RTSP',
+ [Format.MP4_H264]: 'HTTP',
}
-const StatsData: React.FC<
- Omit
-> = ({ format, videoProperties, refresh, volume }) => {
- const [stats, setStats] = useState>([])
-
- // Updates stat values
- const updateValues = useCallback(() => {
- let streamType = 'Unknown'
- if (format === Format.JPEG) {
- streamType = 'Still image'
- } else if (format === Format.MJPEG) {
- streamType = 'MJPEG'
- } else if (format === Format.RTP_H264) {
- streamType = 'RTSP (WebSocket)'
- } else if (format === Format.RTP_JPEG) {
- streamType = 'MJPEG'
- } else if (format === Format.MP4_H264) {
- streamType = 'MP4 (HTTP)'
- }
- const { width, height, pipeline } = videoProperties
- let statsData: Array = [
- {
- name: 'Stream type',
- value: streamType,
- },
- {
- name: 'Resolution',
- value: `${width}x${height}`,
- },
- {
- name: 'Refreshed',
- value: refresh,
- unit: refresh > 1 ? 'times' : 'time',
- },
- ]
- if (isRtspMp4Pipeline(pipeline)) {
- pipeline.mp4.tracks.forEach(({ id, name, codec, bitrate, framerate }) => {
- statsData = statsData.concat([
- {
- name: `Track ${id}`,
- value: `${name} (${codec})`,
- },
- {
- name: 'Frame rate',
- value: framerate.toFixed(2),
- unit: 'fps',
- },
- {
- name: 'Bitrate',
- value: (bitrate / 1000).toFixed(1),
- unit: 'kbit/s',
- },
- ])
- })
- }
-
- if (volume !== undefined) {
- statsData.push({
- name: 'Volume',
- value: Math.floor(volume * 100),
- unit: '%',
- })
- }
-
- setStats(statsData)
- }, [format, refresh, videoProperties, volume])
-
- useEffect(() => {
- updateValues()
- }, [updateValues])
-
- useInterval(updateValues, 1000)
+const volumeLabel = (volume?: number) => {
+ if (volume === undefined) {
+ return ''
+ }
+ const volumeLevel = Math.floor(volume * 100)
+ return `🕪 ${volumeLevel}%`
+}
- return (
-
- {stats.length > 0
- ? stats.map((stat) => {
- return (
-
- {stat.name}
-
- {`${stat.value} ${stat.unit !== undefined ? stat.unit : ''}`}
-
-
- )
- })
- : null}
-
+const bufferLabel = (el: HTMLVideoElement) => {
+ if (el.buffered.length === 0) {
+ return 'buffer: -'
+ }
+ const buffer = Math.floor(
+ (el.buffered.end(el.buffered.length - 1) - el.currentTime) * 1000
)
+ return `buffer: ${String(buffer).padStart(5)} ms`
}
-export const Stats: React.FC = ({
+export function Stats({
format,
videoProperties,
refresh,
volume,
- expanded,
- onToggleExpanded,
-}) => {
- // Handles show/hide stats
- const onToggleStats = useCallback>(
- (e) => {
- e.preventDefault()
- onToggleExpanded(!expanded)
+}: StatsProps) {
+ const [labels, setLabels] = useState()
+ const update = useCallback(
+ (pipeline: PlayerPipeline) => {
+ if (isRtspMp4Pipeline(pipeline)) {
+ setLabels([
+ ...pipeline.mp4.tracks.map(({ name, codec, bitrate, framerate }) => {
+ return `${name} (${codec}) @ ${framerate.toFixed(2)} fps, ${(bitrate / 1000).toFixed(1).padStart(6)} kbit/s`
+ }),
+ bufferLabel(pipeline.videoEl),
+ ])
+ }
+ if (isRtspJpegPipeline(pipeline)) {
+ const { framerate, bitrate } = pipeline
+ setLabels([
+ `JPEG @ ${framerate.toFixed(2)} fps, ${(bitrate / 1000).toFixed(1).padStart(6)} kbit/s`,
+ ])
+ }
+ if (isHttpMp4Pipeline(pipeline)) {
+ const { bitrate } = pipeline
+ setLabels([
+ `${videoProperties.mime ?? 'video/mp4'} @ ${(bitrate / 1000).toFixed(1).padStart(6)} kbit/s (avg)`,
+ bufferLabel(pipeline.mediaElement),
+ ])
+ }
},
- [expanded, onToggleExpanded]
+ [videoProperties]
)
+ useEffect(() => {
+ const { pipeline } = videoProperties
+ if (pipeline) {
+ const refresh = setInterval(() => update(pipeline), 1000)
+ return () => {
+ clearInterval(refresh)
+ setLabels([])
+ }
+ }
+ }, [update, videoProperties.pipeline])
+
+ const { width, height } = videoProperties
+
return (
- <>
- {expanded ? (
-
-
-
-
-
- Client stream data
-
-
- Hide
-
-
-
-
-
- ) : (
-
-
-
-
-
- )}
- >
+
+
{`${streamLabel[format]} ${width}x${height} ${volumeLabel(volume)}, refreshed ${refresh}x`}
+ {labels?.map((label, i) => (
+
+ {label}
+
+ ))}
+
)
}
diff --git a/src/player/StillImage.tsx b/src/player/StillImage.tsx
index 3a3b498d1..0c2e5ced5 100644
--- a/src/player/StillImage.tsx
+++ b/src/player/StillImage.tsx
@@ -1,19 +1,11 @@
import React, { useEffect, useRef } from 'react'
-import styled from 'styled-components'
-
import { VideoProperties } from './PlaybackArea'
import { FORMAT_SUPPORTS_AUDIO } from './constants'
import { useEventState } from './hooks/useEventState'
import { Format } from './types'
import { logDebug } from './utils/log'
-const ImageNative = styled.img`
- max-height: 100%;
- object-fit: contain;
- width: 100%;
-`
-
interface StillImageProps {
readonly forwardedRef?: React.Ref
readonly play?: boolean
@@ -81,5 +73,10 @@ export const StillImage: React.FC = ({
}, [loaded])
logDebug('render image', loaded)
- return
+ return (
+
+ )
}
diff --git a/src/player/WsRtspCanvas.tsx b/src/player/WsRtspCanvas.tsx
index 3ac4751a6..cd9de11cc 100644
--- a/src/player/WsRtspCanvas.tsx
+++ b/src/player/WsRtspCanvas.tsx
@@ -1,6 +1,5 @@
import React, { useEffect, useRef, useState } from 'react'
-import styled from 'styled-components'
import {
Rtcp,
RtspJpegPipeline,
@@ -15,12 +14,6 @@ import { FORMAT_SUPPORTS_AUDIO } from './constants'
import { Format } from './types'
import { logDebug } from './utils/log'
-const CanvasNative = styled.canvas`
- max-height: 100%;
- object-fit: contain;
- width: 100%;
-`
-
interface WsRtspCanvasProps {
readonly forwardedRef?: React.Ref
/**
@@ -224,5 +217,10 @@ export const WsRtspCanvas: React.FC = ({
}
}, [play, pipeline, fetching])
- return
+ return (
+
+ )
}
diff --git a/src/player/WsRtspVideo.tsx b/src/player/WsRtspVideo.tsx
index 59cdb0e79..381689882 100644
--- a/src/player/WsRtspVideo.tsx
+++ b/src/player/WsRtspVideo.tsx
@@ -1,6 +1,5 @@
import React, { useEffect, useRef, useState } from 'react'
-import styled from 'styled-components'
import {
Rtcp,
RtspMp4Pipeline,
@@ -14,7 +13,6 @@ import {
import { Range, VideoProperties } from './PlaybackArea'
import { FORMAT_SUPPORTS_AUDIO } from './constants'
import { useEventState } from './hooks/useEventState'
-import { useVideoDebug } from './hooks/useVideoDebug'
import {
MetadataHandler,
ScheduledMessage,
@@ -23,12 +21,6 @@ import {
import { Format } from './types'
import { logDebug } from './utils/log'
-const VideoNative = styled.video`
- max-height: 100%;
- object-fit: contain;
- width: 100%;
-`
-
/**
* WebSocket + RTSP playback component.
*/
@@ -136,8 +128,6 @@ export const WsRtspVideo: React.FC = ({
const __sensorTmRef = useRef()
- useVideoDebug(videoRef.current)
-
useEffect(() => {
const videoEl = videoRef.current
@@ -161,17 +151,21 @@ export const WsRtspVideo: React.FC = ({
if (__onPlayingRef.current !== undefined) {
__onPlayingRef.current({
el: videoEl,
- pipeline: pipeline ?? undefined,
- width: videoEl.videoWidth,
- height: videoEl.videoHeight,
formatSupportsAudio: FORMAT_SUPPORTS_AUDIO[Format.RTP_H264],
+ height: videoEl.videoHeight,
+ media: pipeline?.mp4.tracks?.map(({ codec, name }) => ({
+ codec,
+ name,
+ })),
+ pipeline: pipeline ?? undefined,
+ range: __rangeRef.current,
+ sensorTm: __sensorTmRef.current,
volume: pipeline?.mp4.tracks?.find((track) =>
track.codec.startsWith('mp4a')
)
? videoEl.volume
: undefined,
- range: __rangeRef.current,
- sensorTm: __sensorTmRef.current,
+ width: videoEl.videoWidth,
})
}
}
@@ -268,5 +262,12 @@ export const WsRtspVideo: React.FC = ({
}
}, [play, pipeline, fetching])
- return
+ return (
+
+ )
}
diff --git a/src/player/components/Button.tsx b/src/player/components/Button.tsx
deleted file mode 100644
index 7beca5464..000000000
--- a/src/player/components/Button.tsx
+++ /dev/null
@@ -1,15 +0,0 @@
-import styled from 'styled-components'
-
-export const Button = styled.button`
- background: transparent;
- fill: white;
- border: none;
- box-sizing: border-box;
- padding: 0;
- margin: 0;
- line-height: 0;
-
- :focus {
- outline: none;
- }
-`
diff --git a/src/player/components/CogWheel.tsx b/src/player/components/CogWheel.tsx
new file mode 100644
index 000000000..cf505ca26
--- /dev/null
+++ b/src/player/components/CogWheel.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+import { controlButtonStyle } from './button-style'
+
+export const CogWheel = ({
+ title,
+ ...buttonProps
+}: React.DOMAttributes & { readonly title?: string }) => {
+ return (
+
+
+ {title !== undefined ? {title} : null}
+
+
+
+
+ )
+}
diff --git a/src/player/components/Limiter.tsx b/src/player/components/Limiter.tsx
index 5c5e79792..f9ac1d5a7 100644
--- a/src/player/components/Limiter.tsx
+++ b/src/player/components/Limiter.tsx
@@ -1,19 +1,27 @@
-import styled from 'styled-components'
+import React, { forwardRef, PropsWithChildren } from 'react'
/**
* The limiter prevents the video element to use up all of the available width.
* The player container will automatically limit it's own height based on the
* available width (keeping aspect ratio).
*/
-export const Limiter = styled.div`
- position: absolute;
- left: 50%;
- transform: translateX(-50%);
- width: 100%;
- top: 0;
- bottom: 0;
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
-`
+export const Limiter = forwardRef(({ children }: PropsWithChildren) => {
+ return (
+
+ {children}
+
+ )
+})
diff --git a/src/player/components/MediaStreamPlayerContainer.tsx b/src/player/components/MediaStreamPlayerContainer.tsx
deleted file mode 100644
index 0156fdeae..000000000
--- a/src/player/components/MediaStreamPlayerContainer.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import styled from 'styled-components'
-
-/**
- * Wrapper for the entire player that will take up all available place from the
- * parent.
- */
-export const MediaStreamPlayerContainer = styled.div`
- position: relative;
- width: 100%;
- height: 100%;
-`
diff --git a/src/player/components/Pause.tsx b/src/player/components/Pause.tsx
new file mode 100644
index 000000000..85daa1de8
--- /dev/null
+++ b/src/player/components/Pause.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+import { controlButtonStyle } from './button-style'
+
+export const Pause = ({
+ title,
+ ...buttonProps
+}: React.DOMAttributes & { readonly title?: string }) => {
+ return (
+
+
+ {title !== undefined ? {title} : null}
+
+
+
+
+ )
+}
diff --git a/src/player/components/Play.tsx b/src/player/components/Play.tsx
new file mode 100644
index 000000000..87622f51e
--- /dev/null
+++ b/src/player/components/Play.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+import { controlButtonStyle } from './button-style'
+
+export const Play = ({
+ title,
+ ...buttonProps
+}: React.DOMAttributes & { readonly title?: string }) => {
+ return (
+
+
+ {title !== undefined ? {title} : null}
+
+
+
+
+ )
+}
diff --git a/src/player/components/Refresh.tsx b/src/player/components/Refresh.tsx
new file mode 100644
index 000000000..6ddb67211
--- /dev/null
+++ b/src/player/components/Refresh.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+import { controlButtonStyle } from './button-style'
+
+export const Refresh = ({
+ title,
+ ...buttonProps
+}: React.DOMAttributes & { readonly title?: string }) => {
+ return (
+
+
+ {title !== undefined ? {title} : null}
+
+
+
+
+ )
+}
diff --git a/src/player/components/Screenshot.tsx b/src/player/components/Screenshot.tsx
new file mode 100644
index 000000000..01c50594d
--- /dev/null
+++ b/src/player/components/Screenshot.tsx
@@ -0,0 +1,23 @@
+import React from 'react'
+import { controlButtonStyle } from './button-style'
+
+export const Screenshot = ({
+ title,
+ ...buttonProps
+}: React.DOMAttributes & { readonly title?: string }) => {
+ return (
+
+
+ {title !== undefined ? {title} : null}
+
+
+
+
+
+ )
+}
diff --git a/src/player/img/Spinner.tsx b/src/player/components/Spinner.tsx
similarity index 100%
rename from src/player/img/Spinner.tsx
rename to src/player/components/Spinner.tsx
diff --git a/src/player/components/Stop.tsx b/src/player/components/Stop.tsx
new file mode 100644
index 000000000..df7e227f5
--- /dev/null
+++ b/src/player/components/Stop.tsx
@@ -0,0 +1,22 @@
+import React from 'react'
+import { controlButtonStyle } from './button-style'
+
+export const Stop = ({
+ title,
+ ...buttonProps
+}: React.DOMAttributes & { readonly title?: string }) => {
+ return (
+
+
+ {title !== undefined ? {title} : null}
+
+
+
+
+ )
+}
diff --git a/src/player/components/Switch.tsx b/src/player/components/Switch.tsx
deleted file mode 100644
index ae7600fd4..000000000
--- a/src/player/components/Switch.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import React, { ChangeEventHandler } from 'react'
-
-import styled from 'styled-components'
-
-const Container = styled.label`
- position: relative;
- display: inline-block;
- width: 28px;
- height: 16px;
-`
-
-const Input = styled.input`
- opacity: 0;
- width: 0;
- height: 0;
-`
-
-const Slider = styled.span`
- border-radius: 16px;
- cursor: pointer;
- position: absolute;
- top: 0;
- right: 0;
- bottom: 0;
- left: 0;
- background-color: #ccc;
- transition: 0.4s;
-
- &:before {
- border-radius: 50%;
- content: '';
- position: absolute;
- height: 12px;
- width: 12px;
- left: 2px;
- bottom: 2px;
- background-color: white;
- transition: 0.4s;
- }
-
- ${Input}:checked + & {
- background-color: #2196f3;
- }
-
- ${Input}:checked + &:before {
- transform: translateX(12px);
- }
-
- ${Input}:focus + & {
- box-shadow: 0 0 1px #2196f3;
- }
-`
-
-export interface SwitchProps {
- readonly name?: string
- readonly checked: boolean
- readonly onChange: ChangeEventHandler
-}
-
-export const Switch: React.FC = (props) => {
- return (
-
-
-
-
- )
-}
diff --git a/src/player/components/button-style.ts b/src/player/components/button-style.ts
new file mode 100644
index 000000000..0f4fe8980
--- /dev/null
+++ b/src/player/components/button-style.ts
@@ -0,0 +1,9 @@
+export const controlButtonStyle = {
+ background: 'transparent',
+ fill: 'white',
+ border: 'none',
+ boxSizing: 'border-box',
+ padding: '0',
+ margin: '0',
+ lineHeight: '0',
+} as const
diff --git a/src/player/img/index.ts b/src/player/components/index.ts
similarity index 85%
rename from src/player/img/index.ts
rename to src/player/components/index.ts
index 15df09d5f..cf403711a 100644
--- a/src/player/img/index.ts
+++ b/src/player/components/index.ts
@@ -5,4 +5,3 @@ export * from './Refresh'
export * from './Screenshot'
export * from './Spinner'
export * from './Stop'
-export * from './StreamStats'
diff --git a/src/player/hooks/useVideoDebug.ts b/src/player/hooks/useVideoDebug.ts
deleted file mode 100644
index 45ddc66f4..000000000
--- a/src/player/hooks/useVideoDebug.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { useEffect } from 'react'
-
-import { logDebug } from '../utils/log'
-
-/**
- * Show debug logs with information received from
- * 'progress' & 'timeupdate' events including the current
- * up time, delay and end time of last buffer.
- * bufferedEnd: the last buffered time
- * currentTime: current playback time
- * delay: the last buffered time - current playback time
- */
-export const useVideoDebug = (videoEl: HTMLVideoElement | null) => {
- useEffect(() => {
- if (videoEl === null) {
- return
- }
-
- // Hacky way of showing delay as a video overlay (don't copy this)
- // but it prevents the console from overflowing with buffer statements
- const stats = document.createElement('div')
- const text = document.createElement('pre')
- stats.appendChild(text)
- videoEl.parentElement?.appendChild(stats)
- stats.setAttribute(
- 'style',
- 'background: rgba(120,255,100,0.4); position: absolute; width: 100px; height: 16px; top: 0; left: 0; font-size: 11px; font-family: "sans";'
- )
- text.setAttribute('style', 'margin: 2px;')
-
- const onUpdate = () => {
- try {
- const currentTime = videoEl.currentTime
- const bufferedEnd = videoEl.buffered.end(videoEl.buffered.length - 1)
-
- const delay = Math.floor((bufferedEnd - currentTime) * 1000)
- const contents = `buffer: ${String(delay).padStart(4, ' ')}ms`
- text.innerText = contents
- } catch (err) {
- logDebug(err)
- }
- }
-
- videoEl.addEventListener('timeupdate', onUpdate)
- videoEl.addEventListener('progress', onUpdate)
-
- return () => {
- videoEl.removeEventListener('timeupdate', onUpdate)
- videoEl.removeEventListener('progress', onUpdate)
- stats.remove()
- }
- }, [videoEl])
-}
diff --git a/src/player/img/CogWheel.tsx b/src/player/img/CogWheel.tsx
deleted file mode 100644
index 0f8884e7e..000000000
--- a/src/player/img/CogWheel.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react'
-
-export const CogWheel = ({ title }: { readonly title?: string }) => {
- return (
-
- {title !== undefined ? {title} : null}
-
-
-
- )
-}
diff --git a/src/player/img/Pause.tsx b/src/player/img/Pause.tsx
deleted file mode 100644
index caf6ecc34..000000000
--- a/src/player/img/Pause.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react'
-
-export const Pause = ({ title }: { readonly title?: string }) => {
- return (
-
- {title !== undefined ? {title} : null}
-
-
-
- )
-}
diff --git a/src/player/img/Play.tsx b/src/player/img/Play.tsx
deleted file mode 100644
index fb492df44..000000000
--- a/src/player/img/Play.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react'
-
-export const Play = ({ title }: { readonly title?: string }) => {
- return (
-
- {title !== undefined ? {title} : null}
-
-
-
- )
-}
diff --git a/src/player/img/Refresh.tsx b/src/player/img/Refresh.tsx
deleted file mode 100644
index dc14ac15e..000000000
--- a/src/player/img/Refresh.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react'
-
-export const Refresh = ({ title }: { readonly title?: string }) => {
- return (
-
- {title !== undefined ? {title} : null}
-
-
-
- )
-}
diff --git a/src/player/img/Screenshot.tsx b/src/player/img/Screenshot.tsx
deleted file mode 100644
index 5f77facb5..000000000
--- a/src/player/img/Screenshot.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from 'react'
-
-export const Screenshot = ({ title }: { readonly title?: string }) => {
- return (
-
- {title !== undefined ? {title} : null}
-
-
-
-
- )
-}
diff --git a/src/player/img/Stop.tsx b/src/player/img/Stop.tsx
deleted file mode 100644
index 3343c7d6b..000000000
--- a/src/player/img/Stop.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import React from 'react'
-
-export const Stop = ({ title }: { readonly title?: string }) => {
- return (
-
- {title !== undefined ? {title} : null}
-
-
-
- )
-}
diff --git a/src/player/img/StreamStats.tsx b/src/player/img/StreamStats.tsx
deleted file mode 100644
index 7ece7fc20..000000000
--- a/src/player/img/StreamStats.tsx
+++ /dev/null
@@ -1,10 +0,0 @@
-import React from 'react'
-
-export const StreamStats = ({ title }: { readonly title?: string }) => {
- return (
-
- {title !== undefined ? {title} : null}
-
-
- )
-}
diff --git a/src/player/playground.tsx b/src/player/playground.tsx
index 8086d15dd..747e8027b 100644
--- a/src/player/playground.tsx
+++ b/src/player/playground.tsx
@@ -7,6 +7,7 @@ const appRoot = createRoot(document.querySelector('#root')!)
appRoot.render(
+
)
diff --git a/src/streams/components/mse-sink.ts b/src/streams/components/mse-sink.ts
index 63ca94ca5..155dd0d6c 100644
--- a/src/streams/components/mse-sink.ts
+++ b/src/streams/components/mse-sink.ts
@@ -147,6 +147,8 @@ async function newSourceBuffer(
}
})
+ mse.duration = 0
+
// // revoke the object URL to avoid a memory leak
window.URL.revokeObjectURL(el.src)
diff --git a/yarn.lock b/yarn.lock
index 558f461a0..3d2923dc1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -69,15 +69,6 @@ __metadata:
languageName: node
linkType: hard
-"@babel/helper-annotate-as-pure@npm:^7.22.5":
- version: 7.25.9
- resolution: "@babel/helper-annotate-as-pure@npm:7.25.9"
- dependencies:
- "@babel/types": ^7.25.9
- checksum: 41edda10df1ae106a9b4fe617bf7c6df77db992992afd46192534f5cff29f9e49a303231733782dd65c5f9409714a529f215325569f14282046e9d3b7a1ffb6c
- languageName: node
- linkType: hard
-
"@babel/helper-compilation-targets@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-compilation-targets@npm:7.25.9"
@@ -91,7 +82,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/helper-module-imports@npm:^7.0.0, @babel/helper-module-imports@npm:^7.22.5, @babel/helper-module-imports@npm:^7.25.9":
+"@babel/helper-module-imports@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/helper-module-imports@npm:7.25.9"
dependencies:
@@ -163,17 +154,6 @@ __metadata:
languageName: node
linkType: hard
-"@babel/plugin-syntax-jsx@npm:^7.22.5":
- version: 7.25.9
- resolution: "@babel/plugin-syntax-jsx@npm:7.25.9"
- dependencies:
- "@babel/helper-plugin-utils": ^7.25.9
- peerDependencies:
- "@babel/core": ^7.0.0-0
- checksum: bb609d1ffb50b58f0c1bac8810d0e46a4f6c922aa171c458f3a19d66ee545d36e782d3bffbbc1fed0dc65a558bdce1caf5279316583c0fff5a2c1658982a8563
- languageName: node
- linkType: hard
-
"@babel/plugin-transform-react-jsx-self@npm:^7.25.9":
version: 7.25.9
resolution: "@babel/plugin-transform-react-jsx-self@npm:7.25.9"
@@ -207,7 +187,7 @@ __metadata:
languageName: node
linkType: hard
-"@babel/traverse@npm:^7.25.9, @babel/traverse@npm:^7.4.5":
+"@babel/traverse@npm:^7.25.9":
version: 7.26.4
resolution: "@babel/traverse@npm:7.26.4"
dependencies:
@@ -373,36 +353,6 @@ __metadata:
languageName: node
linkType: hard
-"@emotion/is-prop-valid@npm:^1.1.0":
- version: 1.3.1
- resolution: "@emotion/is-prop-valid@npm:1.3.1"
- dependencies:
- "@emotion/memoize": ^0.9.0
- checksum: fe6549d54f389e1a17cb02d832af7ee85fb6ea126fc18d02ca47216e8ff19332c1983f4a0ba68602cfcd3b325ffd4ebf0b2d0c6270f1e7e6fe3fca4ba7741e1a
- languageName: node
- linkType: hard
-
-"@emotion/memoize@npm:^0.9.0":
- version: 0.9.0
- resolution: "@emotion/memoize@npm:0.9.0"
- checksum: 038132359397348e378c593a773b1148cd0cf0a2285ffd067a0f63447b945f5278860d9de718f906a74c7c940ba1783ac2ca18f1c06a307b01cc0e3944e783b1
- languageName: node
- linkType: hard
-
-"@emotion/stylis@npm:^0.8.4":
- version: 0.8.5
- resolution: "@emotion/stylis@npm:0.8.5"
- checksum: 67ff5958449b2374b329fb96e83cb9025775ffe1e79153b499537c6c8b2eb64b77f32d7b5d004d646973662356ceb646afd9269001b97c54439fceea3203ce65
- languageName: node
- linkType: hard
-
-"@emotion/unitless@npm:^0.7.4":
- version: 0.7.5
- resolution: "@emotion/unitless@npm:0.7.5"
- checksum: f976e5345b53fae9414a7b2e7a949aa6b52f8bdbcc84458b1ddc0729e77ba1d1dfdff9960e0da60183877873d3a631fa24d9695dd714ed94bcd3ba5196586a6b
- languageName: node
- linkType: hard
-
"@esbuild/aix-ppc64@npm:0.24.2":
version: 0.24.2
resolution: "@esbuild/aix-ppc64@npm:0.24.2"
@@ -876,16 +826,6 @@ __metadata:
languageName: node
linkType: hard
-"@types/hoist-non-react-statics@npm:*":
- version: 3.3.6
- resolution: "@types/hoist-non-react-statics@npm:3.3.6"
- dependencies:
- "@types/react": "*"
- hoist-non-react-statics: ^3.3.0
- checksum: f03e43bd081876c49584ffa0eb690d69991f258203efca44dcc30efdda49a50653ff06402917d1edc9cb7e2adebbe9e2d1d0e739bc99c1b5372103b1cc534e47
- languageName: node
- linkType: hard
-
"@types/istanbul-lib-coverage@npm:^2.0.1":
version: 2.0.6
resolution: "@types/istanbul-lib-coverage@npm:2.0.6"
@@ -932,15 +872,6 @@ __metadata:
languageName: node
linkType: hard
-"@types/react@npm:*":
- version: 19.0.3
- resolution: "@types/react@npm:19.0.3"
- dependencies:
- csstype: ^3.0.2
- checksum: a6c2bcd032522f5c041601a0df1c56288ad66c7973fa672b6c375334dd93295a4d1bfebf9c3498bafb86525f1fd8f4d58175267ed41ef5534b64ba28bd274bb6
- languageName: node
- linkType: hard
-
"@types/react@npm:18.3.18":
version: 18.3.18
resolution: "@types/react@npm:18.3.18"
@@ -965,17 +896,6 @@ __metadata:
languageName: node
linkType: hard
-"@types/styled-components@npm:5.1.34":
- version: 5.1.34
- resolution: "@types/styled-components@npm:5.1.34"
- dependencies:
- "@types/hoist-non-react-statics": "*"
- "@types/react": "*"
- csstype: ^3.0.2
- checksum: 7868066a15afe42d8b353953fc196a7f11a9918b1c980f51d21bb9b49e309c7fe2d730be2d3dc48e92ab4de20f44a3c9dfe6cc98b6ea4dd02655e6f8128171b6
- languageName: node
- linkType: hard
-
"@types/ws@npm:8.5.13":
version: 8.5.13
resolution: "@types/ws@npm:8.5.13"
@@ -994,7 +914,7 @@ __metadata:
languageName: node
linkType: hard
-"@vitejs/plugin-react@npm:4.3.4":
+"@vitejs/plugin-react@npm:4.3.4, @vitejs/plugin-react@npm:^4.3.4":
version: 4.3.4
resolution: "@vitejs/plugin-react@npm:4.3.4"
dependencies:
@@ -1153,21 +1073,6 @@ __metadata:
languageName: node
linkType: hard
-"babel-plugin-styled-components@npm:>= 1.12.0":
- version: 2.1.4
- resolution: "babel-plugin-styled-components@npm:2.1.4"
- dependencies:
- "@babel/helper-annotate-as-pure": ^7.22.5
- "@babel/helper-module-imports": ^7.22.5
- "@babel/plugin-syntax-jsx": ^7.22.5
- lodash: ^4.17.21
- picomatch: ^2.3.1
- peerDependencies:
- styled-components: ">= 2"
- checksum: d791aed68d975dae4f73055f86cd47afa99cb402b8113acdaf5678c8b6fba2cbc15543f2debe8ed09becb198aae8be2adfe268ad41f4bca917288e073a622bf8
- languageName: node
- linkType: hard
-
"balanced-match@npm:^1.0.0":
version: 1.0.2
resolution: "balanced-match@npm:1.0.2"
@@ -1327,13 +1232,6 @@ __metadata:
languageName: node
linkType: hard
-"camelize@npm:^1.0.0":
- version: 1.0.1
- resolution: "camelize@npm:1.0.1"
- checksum: 91d8611d09af725e422a23993890d22b2b72b4cabf7239651856950c76b4bf53fe0d0da7c5e4db05180e898e4e647220e78c9fbc976113bd96d603d1fcbfcb99
- languageName: node
- linkType: hard
-
"caniuse-lite@npm:^1.0.30001688":
version: 1.0.30001690
resolution: "caniuse-lite@npm:1.0.30001690"
@@ -1526,24 +1424,6 @@ __metadata:
languageName: node
linkType: hard
-"css-color-keywords@npm:^1.0.0":
- version: 1.0.0
- resolution: "css-color-keywords@npm:1.0.0"
- checksum: 8f125e3ad477bd03c77b533044bd9e8a6f7c0da52d49bbc0bbe38327b3829d6ba04d368ca49dd9ff3b667d2fc8f1698d891c198bbf8feade1a5501bf5a296408
- languageName: node
- linkType: hard
-
-"css-to-react-native@npm:^3.0.0":
- version: 3.2.0
- resolution: "css-to-react-native@npm:3.2.0"
- dependencies:
- camelize: ^1.0.0
- css-color-keywords: ^1.0.0
- postcss-value-parser: ^4.0.2
- checksum: 263be65e805aef02c3f20c064665c998a8c35293e1505dbe6e3054fb186b01a9897ac6cf121f9840e5a9dfe3fb3994f6fcd0af84a865f1df78ba5bf89e77adce
- languageName: node
- linkType: hard
-
"csstype@npm:^3.0.2":
version: 3.1.3
resolution: "csstype@npm:3.1.3"
@@ -1901,16 +1781,12 @@ __metadata:
version: 0.0.0-use.local
resolution: "example-overlay-react-12c560@workspace:example-overlay-react"
dependencies:
- "@types/react": 18.3.18
- "@types/react-dom": 18.3.5
- "@types/styled-components": 5.1.34
- "@vitejs/plugin-react": 4.3.4
+ "@vitejs/plugin-react": ^4.3.4
media-stream-library: "workspace:^"
- pepjs: 0.5.3
- react: 18.3.1
- react-dom: 18.3.1
- styled-components: 5.3.11
- vite: 6.0.7
+ pepjs: ^0.5.3
+ react: ^18.3.1
+ react-dom: ^18.3.1
+ vite: ^6.0.7
languageName: unknown
linkType: soft
@@ -1918,13 +1794,12 @@ __metadata:
version: 0.0.0-use.local
resolution: "example-player-react-30f25a@workspace:example-player-react"
dependencies:
- "@vitejs/plugin-react": 4.3.4
- luxon: 3.5.0
+ "@vitejs/plugin-react": ^4.3.4
+ luxon: ^3.5.0
media-stream-library: "workspace:^"
- react: 18.3.1
- react-dom: 18.3.1
- styled-components: 5.3.11
- vite: 6.0.7
+ react: ^18.3.1
+ react-dom: ^18.3.1
+ vite: ^6.0.7
languageName: unknown
linkType: soft
@@ -2236,13 +2111,6 @@ __metadata:
languageName: node
linkType: hard
-"has-flag@npm:^3.0.0":
- version: 3.0.0
- resolution: "has-flag@npm:3.0.0"
- checksum: 4a15638b454bf086c8148979aae044dd6e39d63904cd452d970374fa6a87623423da485dfb814e7be882e05c096a7ccf1ebd48e7e7501d0208d8384ff4dea73b
- languageName: node
- linkType: hard
-
"has-flag@npm:^4.0.0":
version: 4.0.0
resolution: "has-flag@npm:4.0.0"
@@ -2275,15 +2143,6 @@ __metadata:
languageName: node
linkType: hard
-"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0":
- version: 3.3.2
- resolution: "hoist-non-react-statics@npm:3.3.2"
- dependencies:
- react-is: ^16.7.0
- checksum: b1538270429b13901ee586aa44f4cc3ecd8831c061d06cb8322e50ea17b3f5ce4d0e2e66394761e6c8e152cd8c34fb3b4b690116c6ce2bd45b18c746516cb9e8
- languageName: node
- linkType: hard
-
"html-encoding-sniffer@npm:^3.0.0":
version: 3.0.0
resolution: "html-encoding-sniffer@npm:3.0.0"
@@ -2718,7 +2577,7 @@ __metadata:
languageName: node
linkType: hard
-"luxon@npm:3.5.0, luxon@npm:^3.0.0":
+"luxon@npm:^3.0.0, luxon@npm:^3.5.0":
version: 3.5.0
resolution: "luxon@npm:3.5.0"
checksum: f290fe5788c8e51e748744f05092160d4be12150dca70f9fadc0d233e53d60ce86acd82e7d909a114730a136a77e56f0d3ebac6141bbb82fd310969a4704825b
@@ -2771,7 +2630,6 @@ __metadata:
"@types/node": 22.10.5
"@types/react": 18.3.18
"@types/react-dom": 18.3.5
- "@types/styled-components": 5.1.34
"@types/ws": 8.5.13
"@vitejs/plugin-react": 4.3.4
base64-js: ^1.5.1
@@ -2788,7 +2646,6 @@ __metadata:
react-dom: 18.3.1
react-is: 18.3.1
semver: 7.6.3
- styled-components: 5.3.11
ts-md5: ^1.3.1
typescript: 5.7.3
uvu: 0.5.6
@@ -2800,7 +2657,6 @@ __metadata:
pepjs: ">= 0.5.3 < 1"
react: ^17.0.1 || ^18.0.0
react-dom: ^17.0.1 || ^18.0.0
- styled-components: ^5.3.5
languageName: unknown
linkType: soft
@@ -3153,7 +3009,7 @@ __metadata:
languageName: node
linkType: hard
-"pepjs@npm:0.5.3":
+"pepjs@npm:0.5.3, pepjs@npm:^0.5.3":
version: 0.5.3
resolution: "pepjs@npm:0.5.3"
checksum: 70ed9e65413d272d8cd055de7224b4c4a94fca12aed27fc5fe7bfd3a0f8e8c1ce172a944acfcafd7e00de06f86044a561c411563c62533d5bbf1c90d287c0086
@@ -3174,13 +3030,6 @@ __metadata:
languageName: node
linkType: hard
-"picomatch@npm:^2.3.1":
- version: 2.3.1
- resolution: "picomatch@npm:2.3.1"
- checksum: 050c865ce81119c4822c45d3c84f1ced46f93a0126febae20737bd05ca20589c564d6e9226977df859ed5e03dc73f02584a2b0faad36e896936238238b0446cf
- languageName: node
- linkType: hard
-
"pify@npm:^2.2.0":
version: 2.3.0
resolution: "pify@npm:2.3.0"
@@ -3199,13 +3048,6 @@ __metadata:
languageName: node
linkType: hard
-"postcss-value-parser@npm:^4.0.2":
- version: 4.2.0
- resolution: "postcss-value-parser@npm:4.2.0"
- checksum: 819ffab0c9d51cf0acbabf8996dffbfafbafa57afc0e4c98db88b67f2094cb44488758f06e5da95d7036f19556a4a732525e84289a425f4f6fd8e412a9d7442f
- languageName: node
- linkType: hard
-
"postcss@npm:^8.4.49":
version: 8.4.49
resolution: "postcss@npm:8.4.49"
@@ -3274,7 +3116,7 @@ __metadata:
languageName: node
linkType: hard
-"react-dom@npm:18.3.1":
+"react-dom@npm:18.3.1, react-dom@npm:^18.3.1":
version: 18.3.1
resolution: "react-dom@npm:18.3.1"
dependencies:
@@ -3293,13 +3135,6 @@ __metadata:
languageName: node
linkType: hard
-"react-is@npm:^16.7.0":
- version: 16.13.1
- resolution: "react-is@npm:16.13.1"
- checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f
- languageName: node
- linkType: hard
-
"react-refresh@npm:^0.14.2":
version: 0.14.2
resolution: "react-refresh@npm:0.14.2"
@@ -3307,7 +3142,7 @@ __metadata:
languageName: node
linkType: hard
-"react@npm:18.3.1":
+"react@npm:18.3.1, react@npm:^18.3.1":
version: 18.3.1
resolution: "react@npm:18.3.1"
dependencies:
@@ -3519,13 +3354,6 @@ __metadata:
languageName: node
linkType: hard
-"shallowequal@npm:^1.1.0":
- version: 1.1.0
- resolution: "shallowequal@npm:1.1.0"
- checksum: f4c1de0837f106d2dbbfd5d0720a5d059d1c66b42b580965c8f06bb1db684be8783538b684092648c981294bf817869f743a066538771dbecb293df78f765e00
- languageName: node
- linkType: hard
-
"shebang-command@npm:^2.0.0":
version: 2.0.0
resolution: "shebang-command@npm:2.0.0"
@@ -3745,37 +3573,6 @@ __metadata:
languageName: node
linkType: hard
-"styled-components@npm:5.3.11":
- version: 5.3.11
- resolution: "styled-components@npm:5.3.11"
- dependencies:
- "@babel/helper-module-imports": ^7.0.0
- "@babel/traverse": ^7.4.5
- "@emotion/is-prop-valid": ^1.1.0
- "@emotion/stylis": ^0.8.4
- "@emotion/unitless": ^0.7.4
- babel-plugin-styled-components: ">= 1.12.0"
- css-to-react-native: ^3.0.0
- hoist-non-react-statics: ^3.0.0
- shallowequal: ^1.1.0
- supports-color: ^5.5.0
- peerDependencies:
- react: ">= 16.8.0"
- react-dom: ">= 16.8.0"
- react-is: ">= 16.8.0"
- checksum: 10edd4dae3b0231ec02d86bdd09c88e894eedfa7e9d4f8e562b09fb69c67a27d586cbcf35c785002d59b3bf11e6c0940b0efce40d13ae9ed148b26b1dc8f3284
- languageName: node
- linkType: hard
-
-"supports-color@npm:^5.5.0":
- version: 5.5.0
- resolution: "supports-color@npm:5.5.0"
- dependencies:
- has-flag: ^3.0.0
- checksum: 95f6f4ba5afdf92f495b5a912d4abee8dcba766ae719b975c56c084f5004845f6f5a5f7769f52d53f40e21952a6d87411bafe34af4a01e65f9926002e38e1dac
- languageName: node
- linkType: hard
-
"supports-color@npm:^7.1.0":
version: 7.2.0
resolution: "supports-color@npm:7.2.0"
@@ -4047,7 +3844,7 @@ __metadata:
languageName: node
linkType: hard
-"vite@npm:6.0.7":
+"vite@npm:6.0.7, vite@npm:^6.0.7":
version: 6.0.7
resolution: "vite@npm:6.0.7"
dependencies: