@banou/media-player 0.0.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.
@@ -0,0 +1,197 @@
1
+ /// <reference types="@emotion/react/types/css-prop" />
2
+ import type { ClassAttributes, HTMLAttributes, MouseEventHandler, MutableRefObject, ReactNode } from 'react'
3
+ import type { Attachment, FKNVideoControl, Subtitle, TransmuxError } from '..'
4
+
5
+ import { useEffect, useMemo, useRef, useState } from 'react'
6
+ import JASSUB from 'jassub'
7
+ import { css } from '@emotion/react'
8
+ import Overlay from './overlay'
9
+ import Bottom from './bottom'
10
+
11
+ const style = css`
12
+ --background-padding: 2rem;
13
+ display: grid;
14
+ grid-template-rows: 1fr;
15
+ overflow: hidden;
16
+
17
+ &.hide {
18
+ cursor: none;
19
+
20
+ .bottom {
21
+ opacity: 0;
22
+ }
23
+ }
24
+
25
+ .bottom {
26
+ &.hide {
27
+ opacity: 0 !important;
28
+ }
29
+ }
30
+ `
31
+
32
+ export type ChromeOptions = {
33
+ customOverlay?: ReactNode
34
+ isPlaying?: boolean
35
+ loading?: boolean
36
+ duration?: number
37
+ loadedTime?: [number, number]
38
+ currentTime?: number
39
+ pictureInPicture: () => void
40
+ fullscreen: () => void
41
+ play: () => void
42
+ seek: (time: number) => void
43
+ getVolume: () => number | undefined
44
+ setVolume: (volume: number) => void
45
+ attachments: Attachment[] | undefined
46
+ tracks: Subtitle[]
47
+ video: MutableRefObject<HTMLVideoElement | undefined>
48
+ errors: TransmuxError[]
49
+ customControls?: FKNVideoControl[]
50
+ libassWorkerUrl: string
51
+ wasmUrl: string
52
+ needsInitialInteraction?: boolean
53
+ } & HTMLAttributes<HTMLDivElement>
54
+
55
+ export default ({
56
+ customOverlay,
57
+ isPlaying,
58
+ loading,
59
+ duration,
60
+ loadedTime,
61
+ currentTime,
62
+ pictureInPicture,
63
+ fullscreen,
64
+ play,
65
+ seek,
66
+ getVolume,
67
+ setVolume,
68
+ attachments,
69
+ tracks,
70
+ video,
71
+ errors,
72
+ customControls,
73
+ libassWorkerUrl,
74
+ wasmUrl,
75
+ needsInitialInteraction,
76
+ ...rest
77
+ }: ChromeOptions) => {
78
+ const [canvasElement, setCanvasElement] = useState<HTMLCanvasElement | undefined>()
79
+ const [canvasInitialized, setCanvasInitialized] = useState(false)
80
+ const [isFullscreen, setFullscreen] = useState(false)
81
+ const [hidden, setHidden] = useState(false)
82
+ const autoHide = useRef<number>()
83
+ const [isSubtitleMenuHidden, setIsSubtitleMenuHidden] = useState(true)
84
+ const [jassub, setJassub] = useState<JASSUB>()
85
+ const [currentSubtitleTrack, setCurrentSubtitleTrack] = useState<number | undefined>()
86
+ const subtitleTrack = useMemo(
87
+ () => currentSubtitleTrack !== undefined ? tracks[currentSubtitleTrack] : undefined,
88
+ [currentSubtitleTrack, currentSubtitleTrack !== undefined && tracks[currentSubtitleTrack]?.data]
89
+ )
90
+ const [isErrorMenuHidden, setIsErrorMenuHidden] = useState(true)
91
+
92
+ const mouseMove: MouseEventHandler<HTMLDivElement> = (ev) => {
93
+ setHidden(false)
94
+ if (autoHide.current) clearInterval(autoHide.current)
95
+ const timeout = setTimeout(() => {
96
+ setHidden(true)
97
+ }, 3_000) as unknown as number
98
+ autoHide.current = timeout
99
+ }
100
+
101
+ // hide automatically after 3 seconds, in case the user doesn't move the mouse on init
102
+ useEffect(() => {
103
+ setTimeout(() => {
104
+ setHidden(true)
105
+ }, 3_000)
106
+ }, [])
107
+
108
+ const mouseOut: React.DOMAttributes<HTMLDivElement>['onMouseOut'] = (ev) => {
109
+ const root = canvasElement?.parentElement?.parentElement
110
+ if (!root?.contains(ev?.relatedTarget as Element)) {
111
+ setHidden(true)
112
+ return
113
+ }
114
+ if (ev.currentTarget.parentElement !== ev.relatedTarget && ev.relatedTarget !== null) return
115
+ setHidden(true)
116
+ }
117
+
118
+ const togglePlay = () => {
119
+ if (!isSubtitleMenuHidden) return
120
+ play()
121
+ }
122
+
123
+ const toggleFullscreen = () => {
124
+ if (!canvasElement || !jassub) return
125
+ setFullscreen(value => !value)
126
+ fullscreen()
127
+ }
128
+
129
+ useEffect(() => {
130
+ if (!video.current || !canvasElement || jassub || !subtitleTrack?.data || !attachments) return
131
+ const fonts = attachments.map(({ filename, data }) => [
132
+ filename.toLowerCase().replaceAll('-', ' ').split('.').at(0),
133
+ data
134
+ ])
135
+ const jassubInstance = new JASSUB({
136
+ video: video.current,
137
+ canvas: canvasElement,
138
+ subContent: subtitleTrack.data,
139
+ fonts: fonts.filter(Boolean).map(([,filename]) => filename as string),
140
+ availableFonts: { ...Object.fromEntries(fonts), 'liberation sans': new URL('/build/default.woff2', new URL(window.location.toString()).origin).toString() },
141
+ workerUrl: libassWorkerUrl, // Link to WebAssembly-based file "libassjs-worker.js",
142
+ modernWasmUrl: wasmUrl
143
+ })
144
+ setJassub(jassubInstance)
145
+ }, [canvasElement, attachments, subtitleTrack?.data])
146
+
147
+ useEffect(() => {
148
+ if (!tracks.length) return
149
+ setCurrentSubtitleTrack(0)
150
+ }, [tracks.length])
151
+
152
+ useEffect(() => {
153
+ if (!jassub) return
154
+ if (!subtitleTrack) {
155
+ jassub.freeTrack()
156
+ return
157
+ }
158
+ jassub.setTrack(subtitleTrack.data)
159
+ const parent = canvasElement?.parentElement
160
+ if (!parent || canvasInitialized) return
161
+ setCanvasInitialized(true)
162
+ }, [jassub, subtitleTrack, canvasInitialized])
163
+
164
+ const setCanvasRef: ClassAttributes<HTMLCanvasElement>['ref'] = (canvasElem) => {
165
+ if (!canvasElem) return
166
+ setCanvasElement(canvasElem)
167
+ }
168
+
169
+ return (
170
+ <div {...rest} css={style} onMouseMove={mouseMove} onMouseOut={mouseOut} className={`chrome ${rest.className ?? ''} ${hidden ? 'hide' : ''}`}>
171
+ <Overlay needsInitialInteraction={needsInitialInteraction} loading={loading} togglePlay={togglePlay} setCanvasRef={setCanvasRef}/>
172
+ {customOverlay}
173
+ <Bottom
174
+ className={`bottom ${needsInitialInteraction ? 'hide' : ''}`}
175
+ toggleFullscreen={toggleFullscreen}
176
+ togglePlay={togglePlay}
177
+ isFullscreen={isFullscreen}
178
+ pictureInPicture={pictureInPicture}
179
+ seek={seek}
180
+ setCurrentSubtitleTrack={setCurrentSubtitleTrack}
181
+ isSubtitleMenuHidden={isSubtitleMenuHidden}
182
+ setIsSubtitleMenuHidden={setIsSubtitleMenuHidden}
183
+ isErrorMenuHidden={isErrorMenuHidden}
184
+ setIsErrorMenuHidden={setIsErrorMenuHidden}
185
+ setVolume={setVolume}
186
+ subtitleTrack={subtitleTrack}
187
+ tracks={tracks}
188
+ currentTime={currentTime}
189
+ duration={duration}
190
+ isPlaying={isPlaying}
191
+ loadedTime={loadedTime}
192
+ errors={errors}
193
+ customControls={customControls}
194
+ />
195
+ </div>
196
+ )
197
+ }
@@ -0,0 +1,104 @@
1
+ /// <reference types="@emotion/react/types/css-prop" />
2
+ import type { ClassAttributes } from 'react'
3
+
4
+ import { css } from '@emotion/react'
5
+
6
+ const style = css`
7
+ position: relative;
8
+ display: grid;
9
+ grid-column: 1;
10
+ grid-row: 1;
11
+ display: grid;
12
+ height: 100%;
13
+ width: 100%;
14
+ justify-items: center;
15
+ align-items: center;
16
+
17
+ canvas {
18
+ pointer-events: none;
19
+ position: absolute;
20
+ inset: 0;
21
+ grid-column: 1;
22
+ grid-row: 1;
23
+ height: 100%;
24
+ width: 100%;
25
+ }
26
+
27
+ .loading {
28
+ grid-column: 1;
29
+ grid-row: 1;
30
+ }
31
+ `
32
+
33
+ export type OverlayOptions = {
34
+ loading?: boolean
35
+ togglePlay: (ev: any) => void
36
+ setCanvasRef: ClassAttributes<HTMLCanvasElement>['ref'],
37
+ needsInitialInteraction?: boolean
38
+ }
39
+
40
+ export default ({ loading, needsInitialInteraction, togglePlay, setCanvasRef }: OverlayOptions) => {
41
+ return (
42
+ <div css={style} onClick={togglePlay}>
43
+ <canvas ref={setCanvasRef}/>
44
+ {
45
+ needsInitialInteraction
46
+ ? (
47
+ <svg
48
+ xmlns="http://www.w3.org/2000/svg"
49
+ width="60"
50
+ height="60"
51
+ viewBox="0 0 60 60"
52
+ fill="black"
53
+ stroke="currentColor"
54
+ strokeWidth="3"
55
+ strokeLinecap="round"
56
+ strokeLinejoin="round"
57
+ className="feather feather-play"
58
+ >
59
+ <polygon points="12 7 45 28 12 50 12 7"></polygon>
60
+ </svg>
61
+ )
62
+ : (
63
+ loading
64
+ ? (
65
+ <svg
66
+ className="loading"
67
+ xmlns="http://www.w3.org/2000/svg"
68
+ style={{
69
+ display: 'block',
70
+ shapeRendering: 'auto',
71
+ animationPlayState: 'running',
72
+ animationDelay: '0s',
73
+ }}
74
+ width="100" height="100"
75
+ viewBox="0 0 100 100"
76
+ preserveAspectRatio="xMidYMid"
77
+ >
78
+ <circle
79
+ cx="50"
80
+ cy="50"
81
+ fill="none"
82
+ stroke="currentColor"
83
+ strokeWidth="9"
84
+ r="35"
85
+ strokeDasharray="164.93361431346415 56.97787143782138"
86
+ style={{ animationPlayState: 'running', animationDelay: '0s' }}>
87
+ <animateTransform
88
+ attributeName="transform"
89
+ type="rotate"
90
+ repeatCount="indefinite"
91
+ dur="1s"
92
+ values="0 50 50;360 50 50"
93
+ keyTimes="0;1"
94
+ style={{ animationPlayState: 'running', animationDelay: '0s' }}
95
+ />
96
+ </circle>
97
+ </svg>
98
+ )
99
+ : null
100
+ )
101
+ }
102
+ </div>
103
+ )
104
+ }