@banou/media-player 0.6.2 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +148 -3
  2. package/dist/engine/index.d.ts +10 -0
  3. package/dist/engine/index.js +2 -0
  4. package/dist/engine/picture-in-picture.d.ts +27 -0
  5. package/dist/engine/playback.d.ts +45 -0
  6. package/dist/engine/source-buffer.d.ts +12 -0
  7. package/dist/engine/subtitles.d.ts +36 -0
  8. package/dist/engine/thumbnails.d.ts +22 -0
  9. package/dist/engine-Dmc7JSd9.js +470 -0
  10. package/dist/index.d.ts +12 -0
  11. package/dist/index.js +1566 -0
  12. package/dist/react/components/chrome.d.ts +9 -0
  13. package/dist/react/components/control-bar.d.ts +2 -0
  14. package/dist/react/components/overlay.d.ts +4 -0
  15. package/dist/react/components/playback-slider.d.ts +2 -0
  16. package/dist/react/components/progress-bar.d.ts +2 -0
  17. package/dist/react/components/settings.d.ts +2 -0
  18. package/dist/react/components/sound.d.ts +7 -0
  19. package/{build → dist/react}/components/tooltip-display.d.ts +8 -8
  20. package/dist/react/components/volume-slider.d.ts +6 -0
  21. package/dist/react/hooks/use-drag-value.d.ts +23 -0
  22. package/dist/react/hooks/use-picture-in-picture.d.ts +2 -0
  23. package/dist/react/hooks/use-playback.d.ts +6 -0
  24. package/dist/react/hooks/use-thumbnails.d.ts +11 -0
  25. package/dist/react/player.d.ts +25 -0
  26. package/dist/react/source-feature.d.ts +84 -0
  27. package/dist/react/video-player.d.ts +43 -0
  28. package/dist/utils/source.d.ts +30 -0
  29. package/dist/utils/track-label.d.ts +19 -0
  30. package/dist/utils/volume-utils.d.ts +6 -0
  31. package/package.json +49 -37
  32. package/src/lib/engine/index.ts +14 -0
  33. package/src/lib/engine/picture-in-picture.ts +250 -0
  34. package/src/lib/engine/playback.ts +352 -0
  35. package/src/lib/engine/source-buffer.ts +61 -0
  36. package/src/lib/engine/subtitles.ts +210 -0
  37. package/src/lib/engine/thumbnails.ts +137 -0
  38. package/src/lib/globals.d.ts +8 -0
  39. package/src/lib/index.tsx +36 -0
  40. package/src/lib/react/components/chrome.tsx +126 -0
  41. package/src/{components → lib/react/components}/control-bar.tsx +74 -96
  42. package/src/lib/react/components/overlay.tsx +121 -0
  43. package/src/{components → lib/react/components}/playback-slider.tsx +30 -52
  44. package/src/{components → lib/react/components}/progress-bar.tsx +128 -41
  45. package/src/lib/react/components/settings.tsx +391 -0
  46. package/src/lib/react/components/sound.tsx +133 -0
  47. package/src/{components → lib/react/components}/tooltip-display.tsx +11 -8
  48. package/src/{components → lib/react/components}/volume-slider.tsx +32 -46
  49. package/src/lib/react/hooks/use-drag-value.ts +73 -0
  50. package/src/lib/react/hooks/use-picture-in-picture.ts +36 -0
  51. package/src/lib/react/hooks/use-playback.ts +134 -0
  52. package/src/lib/react/hooks/use-thumbnails.ts +77 -0
  53. package/src/lib/react/player.ts +20 -0
  54. package/src/lib/react/source-feature.ts +90 -0
  55. package/src/lib/react/video-player.tsx +119 -0
  56. package/src/lib/utils/source.ts +60 -0
  57. package/src/lib/utils/track-label.ts +36 -0
  58. package/src/lib/utils/volume-utils.ts +13 -0
  59. package/build/components/chrome.d.ts +0 -7
  60. package/build/components/control-bar.d.ts +0 -6
  61. package/build/components/overlay.d.ts +0 -4
  62. package/build/components/playback-slider.d.ts +0 -2
  63. package/build/components/progress-bar.d.ts +0 -1
  64. package/build/components/settings.d.ts +0 -2
  65. package/build/components/sound.d.ts +0 -4
  66. package/build/components/volume-slider.d.ts +0 -6
  67. package/build/index.d.ts +0 -26
  68. package/build/index.js +0 -3416
  69. package/build/main.d.ts +0 -1
  70. package/build/state-machines/data-source.d.ts +0 -27
  71. package/build/state-machines/index.d.ts +0 -34038
  72. package/build/state-machines/media-properties.d.ts +0 -45
  73. package/build/state-machines/media-source.d.ts +0 -34
  74. package/build/state-machines/media.d.ts +0 -8558
  75. package/build/state-machines/subtitles.d.ts +0 -56
  76. package/build/state-machines/thumbnails.d.ts +0 -24
  77. package/build/state-machines/utils.d.ts +0 -26
  78. package/build/utils/actor-utils.d.ts +0 -2
  79. package/build/utils/context.d.ts +0 -14
  80. package/build/utils/index.d.ts +0 -4
  81. package/build/utils/languages.d.ts +0 -260
  82. package/build/utils/mp4box.d.ts +0 -61
  83. package/build/utils/use-local-storage.d.ts +0 -3
  84. package/build/utils/use-scrub.d.ts +0 -11
  85. package/build/utils/volume-utils.d.ts +0 -17
  86. package/build/utils/window-height.d.ts +0 -2
  87. package/src/components/chrome.tsx +0 -95
  88. package/src/components/overlay.tsx +0 -91
  89. package/src/components/settings.tsx +0 -322
  90. package/src/components/sound.tsx +0 -101
  91. package/src/index.tsx +0 -195
  92. package/src/main.tsx +0 -169
  93. package/src/state-machines/data-source.ts +0 -84
  94. package/src/state-machines/index.ts +0 -5
  95. package/src/state-machines/media-properties.ts +0 -82
  96. package/src/state-machines/media-source.ts +0 -129
  97. package/src/state-machines/media.ts +0 -255
  98. package/src/state-machines/subtitles.ts +0 -285
  99. package/src/state-machines/thumbnails.ts +0 -123
  100. package/src/state-machines/utils.ts +0 -94
  101. package/src/utils/actor-utils.ts +0 -19
  102. package/src/utils/context.ts +0 -23
  103. package/src/utils/index.ts +0 -229
  104. package/src/utils/languages.ts +0 -262
  105. package/src/utils/mp4box.ts +0 -74
  106. package/src/utils/use-local-storage.ts +0 -30
  107. package/src/utils/use-scrub.ts +0 -40
  108. package/src/utils/volume-utils.ts +0 -39
  109. package/src/utils/window-height.ts +0 -19
  110. package/src/vite-env.d.ts +0 -1
  111. package/tsconfig.json +0 -28
  112. package/tsconfig.node.json +0 -9
  113. /package/{build → dist}/utils/colors.d.ts +0 -0
  114. /package/{build → dist}/utils/fonts.d.ts +0 -0
  115. /package/{build → dist}/utils/time.d.ts +0 -0
  116. /package/src/{assets → lib/assets}/picture-in-picture.svg +0 -0
  117. /package/src/{utils → lib/utils}/colors.ts +0 -0
  118. /package/src/{utils → lib/utils}/fonts.ts +0 -0
  119. /package/src/{utils → lib/utils}/time.ts +0 -0
@@ -0,0 +1,90 @@
1
+ import type { AudioStream, MediaIndex, SubtitleStream, ThumbnailImage } from '../engine'
2
+
3
+ import { definePlayerFeature } from '@videojs/core/dom'
4
+
5
+ /**
6
+ * A byte span of the file the consumer has in hand, mapped onto the timeline through the keyframe
7
+ * index, because a file's download percentage is not its playback percentage.
8
+ */
9
+ export type DownloadedRange = {
10
+ startByteOffset: number
11
+ endByteOffset: number
12
+ }
13
+
14
+ /**
15
+ * Everything about the source that the chrome reads, carried on the player store next to the built-in
16
+ * playback state so a component never has to know which of two channels owns a field.
17
+ *
18
+ * The store is the single reader surface; the React layer is the single writer, through
19
+ * `setSourceState`.
20
+ */
21
+ export type SourceState = {
22
+ /** Shown top left over the video. */
23
+ title?: string
24
+ /** Total byte length of the source. */
25
+ size?: number
26
+ downloadedRanges?: DownloadedRange[]
27
+
28
+ /** Keyframe index of the input, which turns a downloaded byte range into a time range. */
29
+ indexes: MediaIndex[]
30
+ thumbnails: ThumbnailImage[]
31
+
32
+ subtitleStreams: SubtitleStream[]
33
+ /** undefined means subtitles are off. */
34
+ selectedSubtitleStream: number | undefined
35
+ selectSubtitleStream: (streamIndex: number | undefined) => void
36
+
37
+ audioStreams: AudioStream[]
38
+ selectedAudioStream: number
39
+ selectAudioStream: (streamIndex: number) => void
40
+
41
+ /** Chrome auto-hide. True means the controls, title and cursor are hidden. */
42
+ hideUI: boolean
43
+ setHideUI: (hide: boolean) => void
44
+
45
+ /** Owned here, not by `pip`: that watches the media element, and the window holds a mirror. */
46
+ togglePictureInPicture: () => void
47
+
48
+ /** Set when the pipeline fails. Cleared when it recovers. */
49
+ playbackError: unknown
50
+ /** Whether the engine has produced its first media segment. */
51
+ ready: boolean
52
+
53
+ /**
54
+ * The write seam, wired in `attach`. Only the React layer calls it.
55
+ *
56
+ * It is a no-op until the media element attaches. Every caller writes either from an engine
57
+ * callback or after `startPlayback` resolves, both of which are far later than attach, and the one
58
+ * synchronous write is a reset to the values the defaults below already hold.
59
+ */
60
+ setSourceState: (partial: Partial<SourceState>) => void
61
+ }
62
+
63
+ const initialState: SourceState = {
64
+ indexes: [],
65
+ thumbnails: [],
66
+ subtitleStreams: [],
67
+ selectedSubtitleStream: undefined,
68
+ selectSubtitleStream: () => {},
69
+ audioStreams: [],
70
+ selectedAudioStream: -1,
71
+ selectAudioStream: () => {},
72
+ hideUI: false,
73
+ setHideUI: () => {},
74
+ togglePictureInPicture: () => {},
75
+ playbackError: null,
76
+ ready: false,
77
+ setSourceState: () => {},
78
+ }
79
+
80
+ export const sourceFeature = definePlayerFeature({
81
+ name: 'source',
82
+ state: () => ({ ...initialState }),
83
+ attach({ set }) {
84
+ set({
85
+ setSourceState: (partial) => set(partial),
86
+ // needs nothing from the pipeline, so it is answered here rather than pushed in from React
87
+ setHideUI: (hideUI) => set({ hideUI }),
88
+ })
89
+ },
90
+ })
@@ -0,0 +1,119 @@
1
+ /// <reference types="@emotion/react/types/css-prop" />
2
+ import type { ReactNode } from 'react'
3
+ import type { DownloadedRange } from './source-feature'
4
+
5
+ import { useEffect, useState } from 'react'
6
+ import { css } from '@emotion/react'
7
+ import { useContainerAttach, useMediaAttach } from '@videojs/react'
8
+
9
+ import { Player, usePlayer } from './player'
10
+ import { usePlayback } from './hooks/use-playback'
11
+ import { useSeekThumbnails } from './hooks/use-thumbnails'
12
+ import { usePictureInPicture } from './hooks/use-picture-in-picture'
13
+ import Chrome from './components/chrome'
14
+
15
+ /**
16
+ * The source, read a range at a time: the player never downloads the whole file. Both fields travel
17
+ * together, so passing one without the other is a type error.
18
+ */
19
+ export type MediaPlayerSource =
20
+ | { read: (offset: number, size: number) => Promise<ArrayBuffer>, size: number }
21
+ | { read?: undefined, size?: undefined }
22
+
23
+ export type MediaPlayerOptions =
24
+ & MediaPlayerSource
25
+ & {
26
+ /**
27
+ * Where libav's wasm is served from. BOTH `libav.wasm` and `libav-jspi.wasm` have to be there:
28
+ * libav-wasm picks between them on `WebAssembly.Suspending`, so serving one fails only on the
29
+ * browsers that pick the other.
30
+ */
31
+ publicPath: string
32
+ libavWorkerUrl: string
33
+ jassubWorkerUrl: string
34
+ /** jassub's SIMD build, `jassub-worker-modern.wasm`. */
35
+ jassubWasmUrl: string
36
+ /**
37
+ * jassub's non-SIMD build, `jassub-worker.wasm`, for Safari before 16.4.
38
+ * Without it subtitles fail outright on those browsers rather than falling back to the slower build.
39
+ */
40
+ jassubLegacyWasmUrl?: string
41
+ /** Fallback face for `liberation sans`, used when a subtitle track names a font the file does not carry. */
42
+ defaultFontUrl?: string
43
+ bufferSize?: number
44
+ autoplay?: boolean
45
+
46
+ title?: string
47
+ /** Byte spans available, painted on the seekbar and informing the thumbnail generator. */
48
+ downloadedRanges?: DownloadedRange[]
49
+
50
+ onSeek?: (fraction: number) => void
51
+ onPlaybackError?: (error: unknown) => void
52
+ }
53
+
54
+ const PlayerRoot = ({ options, children }: { options: MediaPlayerOptions, children?: ReactNode }) => {
55
+ const { title, size, downloadedRanges, publicPath, libavWorkerUrl, read } = options
56
+
57
+ const setMedia = useMediaAttach()
58
+ // Mandatory, not an optimisation: without it requestFullscreen falls through to the bare <video>
59
+ // and leaves the whole chrome outside the fullscreen layer.
60
+ const setContainer = useContainerAttach()
61
+
62
+ const [video, setVideo] = useState<HTMLVideoElement | null>(null)
63
+ const [canvas, setCanvas] = useState<HTMLCanvasElement | null>(null)
64
+
65
+ useEffect(() => { setMedia?.(video); return () => setMedia?.(null) }, [video, setMedia])
66
+
67
+ usePlayback(video, canvas, options)
68
+
69
+ const thumbnails = useSeekThumbnails({
70
+ publicPath,
71
+ workerUrl: libavWorkerUrl,
72
+ length: size,
73
+ read,
74
+ downloadedRanges,
75
+ })
76
+ const togglePictureInPicture = usePictureInPicture(video, canvas)
77
+
78
+ // Subscribed rather than read off the store, because it is a no-op until the media element
79
+ // attaches: when attach swaps in the real setter the identity changes and these publish again.
80
+ const setSourceState = usePlayer((state) => state.setSourceState)
81
+
82
+ useEffect(() => {
83
+ setSourceState({ title, size, downloadedRanges })
84
+ }, [setSourceState, title, size, downloadedRanges])
85
+
86
+ useEffect(() => {
87
+ setSourceState({ thumbnails, togglePictureInPicture })
88
+ }, [setSourceState, thumbnails, togglePictureInPicture])
89
+
90
+ return (
91
+ <Chrome
92
+ ref={setContainer}
93
+ onVideoRef={setVideo}
94
+ onCanvasRef={setCanvas}
95
+ >
96
+ {children}
97
+ </Chrome>
98
+ )
99
+ }
100
+
101
+ // #111 against the black inside is deliberate: this is the letterbox around the player box.
102
+ const rootStyle = css`
103
+ display: flex;
104
+ justify-content: center;
105
+ background-color: #111;
106
+ height: 100%;
107
+ width: 100%;
108
+ overflow: hidden;
109
+ `
110
+
111
+ export const MediaPlayer = (options: MediaPlayerOptions & { children?: ReactNode }) => (
112
+ <div css={rootStyle}>
113
+ <Player.Provider>
114
+ <PlayerRoot options={options}>{options.children}</PlayerRoot>
115
+ </Player.Provider>
116
+ </div>
117
+ )
118
+
119
+ export default MediaPlayer
@@ -0,0 +1,60 @@
1
+ const fromBlob = ({ blob, name }: { blob: Blob, name?: string }) => ({
2
+ length: blob.size,
3
+ name: name ?? (blob instanceof File ? blob.name : undefined),
4
+ read: (offset: number, size: number) => blob.slice(offset, offset + size).arrayBuffer(),
5
+ })
6
+
7
+ const probeLength = async (url: string, credentials?: RequestCredentials) => {
8
+ const response = await fetch(url, { headers: { Range: 'bytes=0-1' }, credentials })
9
+ if (!response.ok) throw new Error(`The source could not be read: HTTP ${response.status}`)
10
+ const fromRange = response.headers.get('Content-Range')?.split('/').at(1)
11
+ const length = fromRange ? Number(fromRange) : Number(response.headers.get('Content-Length'))
12
+ if (!Number.isFinite(length) || length <= 0) throw new Error('The source did not report its length')
13
+ if (!fromRange && response.status !== 206) throw new Error('The source does not support range requests')
14
+ return length
15
+ }
16
+
17
+ const fromUrl = async (
18
+ { url, length: _length, name, credentials }: {
19
+ url: string
20
+ length?: number
21
+ name?: string
22
+ credentials?: RequestCredentials
23
+ }
24
+ ) => {
25
+ const length = _length ?? await probeLength(url, credentials)
26
+ const read = async (offset: number, size: number) => {
27
+ const response = await fetch(url, {
28
+ headers: { Range: `bytes=${offset}-${Math.min(offset + size, length) - 1}` },
29
+ credentials,
30
+ })
31
+ if (!response.ok) throw new Error(`The source could not be read: HTTP ${response.status}`)
32
+ return response.arrayBuffer()
33
+ }
34
+ return { length, name, read }
35
+ }
36
+
37
+ /** A source resolved down to what the remuxer needs: a total length and a byte-range reader. */
38
+ export type RemuxerInput = {
39
+ length: number
40
+ name?: string
41
+ read: (offset: number, size: number) => Promise<ArrayBuffer>
42
+ }
43
+
44
+ export const inputToRemuxerInput = async (
45
+ params:
46
+ | Parameters<typeof fromBlob | typeof fromUrl>[0]
47
+ // the general arm. `length` is required: nothing downstream can seek or report a duration
48
+ // without it, and a caller supplying its own reader already knows the total.
49
+ | {
50
+ length: number
51
+ name?: string
52
+ read: (offset: number, size: number) => Promise<ArrayBuffer>
53
+ }
54
+ // declared rather than inferred, so all three arms are checked against one contract
55
+ ): Promise<RemuxerInput> => {
56
+ if ('blob' in params) return fromBlob(params)
57
+ if ('url' in params) return fromUrl(params)
58
+ if ('read' in params) return { length: params.length, name: params.name, read: params.read }
59
+ throw new Error(`Unknown source type: ${params}`)
60
+ }
@@ -0,0 +1,36 @@
1
+ export type LabelledTrack = {
2
+ streamIndex: number
3
+ title: string
4
+ language: string
5
+ }
6
+
7
+ /**
8
+ * Human name for a track's language tag. `fallback: 'none'` is what makes an unknown tag come back
9
+ * undefined instead of echoing itself, so the caller can fall through to the title.
10
+ */
11
+ export const displayLanguage = (language: string | undefined): string | undefined => {
12
+ if (!language || language === 'und') return undefined
13
+ try {
14
+ return new Intl.DisplayNames([navigator.language || 'en'], { type: 'language', fallback: 'none' }).of(language) || undefined
15
+ } catch {
16
+ return undefined
17
+ }
18
+ }
19
+
20
+ const tidy = (title: string | undefined) => title?.replace(/_/g, ' ').trim() || undefined
21
+
22
+ /**
23
+ * Names a set of tracks for a menu, by language, falling back to the title then the stream index.
24
+ * The title is appended only to break a tie, because on a MultiSub release every title is the same
25
+ * source tag and titling every row gives nine identical entries.
26
+ */
27
+ export const labelTracks = <T extends LabelledTrack>(tracks: T[]): { track: T, label: string }[] => {
28
+ const bases = tracks.map((track) => displayLanguage(track.language) ?? tidy(track.title) ?? `Track ${track.streamIndex}`)
29
+ const counts = bases.reduce<Record<string, number>>((acc, base) => ({ ...acc, [base]: (acc[base] ?? 0) + 1 }), {})
30
+ return tracks.map((track, index) => {
31
+ const base = bases[index]!
32
+ const detail = tidy(track.title)
33
+ const ambiguous = (counts[base] ?? 0) > 1
34
+ return { track, label: ambiguous && detail && detail !== base ? `${base} (${detail})` : base }
35
+ })
36
+ }
@@ -0,0 +1,13 @@
1
+ const MIN_VOLUME = 0.0001
2
+ const MAX_VOLUME = 1.0
3
+
4
+ /** Higher values put more of the slider's travel at the quiet end. */
5
+ export const VOLUME_EXPONENT = 2.0
6
+
7
+ /** Slider position (0-1) to gain, on a curve that matches how loudness is perceived. */
8
+ export const linearToLogVolume = (linearValue: number): number =>
9
+ Math.pow(Math.max(MIN_VOLUME, Math.min(MAX_VOLUME, linearValue)), VOLUME_EXPONENT)
10
+
11
+ /** Gain (0-1) back to slider position. */
12
+ export const logToLinearVolume = (logVolume: number): number =>
13
+ Math.pow(Math.max(MIN_VOLUME, Math.min(MAX_VOLUME, logVolume)), MAX_VOLUME / VOLUME_EXPONENT)
@@ -1,7 +0,0 @@
1
- import { ReactNode, type HTMLAttributes } from 'react';
2
- export type ChromeOptions = {
3
- mediaInformation?: ReactNode;
4
- loadingInformation?: ReactNode;
5
- } & HTMLAttributes<HTMLDivElement>;
6
- export declare const Chrome: ({ children, mediaInformation, loadingInformation }: ChromeOptions) => import("react/jsx-runtime").JSX.Element;
7
- export default Chrome;
@@ -1,6 +0,0 @@
1
- import { ReactNode, RefObject } from 'react';
2
- export declare const ControlBar: ({ mediaInformation, containerRef }: {
3
- mediaInformation?: ReactNode;
4
- containerRef: RefObject<HTMLDivElement>;
5
- }) => import("react/jsx-runtime").JSX.Element;
6
- export default ControlBar;
@@ -1,4 +0,0 @@
1
- import { ReactNode } from 'react';
2
- export declare const Overlay: ({ loadingInformation }: {
3
- loadingInformation?: ReactNode;
4
- }) => import("react/jsx-runtime").JSX.Element;
@@ -1,2 +0,0 @@
1
- declare const PlaybackSlider: () => import("react/jsx-runtime").JSX.Element;
2
- export default PlaybackSlider;
@@ -1 +0,0 @@
1
- export declare const ProgressBar: () => import("react/jsx-runtime").JSX.Element;
@@ -1,2 +0,0 @@
1
- declare const SettingsAction: () => import("react/jsx-runtime").JSX.Element;
2
- export default SettingsAction;
@@ -1,4 +0,0 @@
1
- declare const Sound: ({ ref }: {
2
- ref: any;
3
- }) => import("react/jsx-runtime").JSX.Element;
4
- export default Sound;
@@ -1,6 +0,0 @@
1
- type VolumeSliderType = {
2
- value: number;
3
- onChange: (v: number) => void;
4
- };
5
- declare const VolumeSlider: ({ value, onChange }: VolumeSliderType) => import("react/jsx-runtime").JSX.Element;
6
- export default VolumeSlider;
package/build/index.d.ts DELETED
@@ -1,26 +0,0 @@
1
- import type { MutableRefObject, ReactNode, RefCallback } from 'react';
2
- import { DownloadedRange } from './utils/context';
3
- export type FKNVideoOptions = {
4
- title?: string;
5
- downloadedRanges?: DownloadedRange[];
6
- read?: (offset: number, size: number) => Promise<ArrayBuffer>;
7
- size?: number;
8
- bufferSize?: number;
9
- autoplay?: boolean;
10
- mediaInformation?: ReactNode;
11
- loadingInformation?: ReactNode;
12
- publicPath: string;
13
- jassubWorkerUrl: string;
14
- jassubWasmUrl: string;
15
- jassubModernWasmUrl: string;
16
- libavWorkerUrl: string;
17
- };
18
- export declare const FKNVideoRoot: ({ options, videoElement, children }: {
19
- options: FKNVideoOptions;
20
- videoElement: HTMLVideoElement | undefined;
21
- children: ReactNode;
22
- }) => import("react/jsx-runtime").JSX.Element;
23
- declare const FKNVideo: ({ ref, ...options }: FKNVideoOptions & {
24
- ref?: RefCallback<HTMLVideoElement> | MutableRefObject<HTMLVideoElement | null>;
25
- }) => import("react/jsx-runtime").JSX.Element;
26
- export default FKNVideo;