@banou/media-player 0.6.2 → 0.8.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 (123) hide show
  1. package/README.md +149 -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 +14 -0
  11. package/dist/index.js +1610 -0
  12. package/dist/react/components/chrome.d.ts +12 -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 +8 -0
  24. package/dist/react/hooks/use-thumbnails.d.ts +12 -0
  25. package/dist/react/media.d.ts +98 -0
  26. package/dist/react/player.d.ts +26 -0
  27. package/dist/react/source-feature.d.ts +106 -0
  28. package/dist/react/video-player.d.ts +83 -0
  29. package/dist/utils/source.d.ts +30 -0
  30. package/dist/utils/track-label.d.ts +31 -0
  31. package/dist/utils/volume-utils.d.ts +6 -0
  32. package/package.json +58 -37
  33. package/src/lib/engine/index.ts +14 -0
  34. package/src/lib/engine/picture-in-picture.ts +250 -0
  35. package/src/lib/engine/playback.ts +352 -0
  36. package/src/lib/engine/source-buffer.ts +61 -0
  37. package/src/lib/engine/subtitles.ts +210 -0
  38. package/src/lib/engine/thumbnails.ts +137 -0
  39. package/src/lib/globals.d.ts +8 -0
  40. package/src/lib/index.tsx +51 -0
  41. package/src/lib/react/components/chrome.tsx +130 -0
  42. package/src/{components → lib/react/components}/control-bar.tsx +74 -96
  43. package/src/lib/react/components/overlay.tsx +125 -0
  44. package/src/{components → lib/react/components}/playback-slider.tsx +30 -52
  45. package/src/lib/react/components/progress-bar.tsx +341 -0
  46. package/src/lib/react/components/settings.tsx +384 -0
  47. package/src/lib/react/components/sound.tsx +133 -0
  48. package/src/{components → lib/react/components}/tooltip-display.tsx +15 -12
  49. package/src/{components → lib/react/components}/volume-slider.tsx +32 -46
  50. package/src/lib/react/hooks/use-drag-value.ts +73 -0
  51. package/src/lib/react/hooks/use-picture-in-picture.ts +36 -0
  52. package/src/lib/react/hooks/use-playback.ts +146 -0
  53. package/src/lib/react/hooks/use-thumbnails.ts +78 -0
  54. package/src/lib/react/media.ts +124 -0
  55. package/src/lib/react/player.ts +20 -0
  56. package/src/lib/react/source-feature.ts +108 -0
  57. package/src/lib/react/video-player.tsx +223 -0
  58. package/src/lib/utils/fonts.ts +160 -0
  59. package/src/lib/utils/source.ts +60 -0
  60. package/src/lib/utils/track-label.ts +51 -0
  61. package/src/lib/utils/volume-utils.ts +13 -0
  62. package/build/components/chrome.d.ts +0 -7
  63. package/build/components/control-bar.d.ts +0 -6
  64. package/build/components/overlay.d.ts +0 -4
  65. package/build/components/playback-slider.d.ts +0 -2
  66. package/build/components/progress-bar.d.ts +0 -1
  67. package/build/components/settings.d.ts +0 -2
  68. package/build/components/sound.d.ts +0 -4
  69. package/build/components/volume-slider.d.ts +0 -6
  70. package/build/index.d.ts +0 -26
  71. package/build/index.js +0 -3416
  72. package/build/main.d.ts +0 -1
  73. package/build/state-machines/data-source.d.ts +0 -27
  74. package/build/state-machines/index.d.ts +0 -34038
  75. package/build/state-machines/media-properties.d.ts +0 -45
  76. package/build/state-machines/media-source.d.ts +0 -34
  77. package/build/state-machines/media.d.ts +0 -8558
  78. package/build/state-machines/subtitles.d.ts +0 -56
  79. package/build/state-machines/thumbnails.d.ts +0 -24
  80. package/build/state-machines/utils.d.ts +0 -26
  81. package/build/utils/actor-utils.d.ts +0 -2
  82. package/build/utils/context.d.ts +0 -14
  83. package/build/utils/index.d.ts +0 -4
  84. package/build/utils/languages.d.ts +0 -260
  85. package/build/utils/mp4box.d.ts +0 -61
  86. package/build/utils/use-local-storage.d.ts +0 -3
  87. package/build/utils/use-scrub.d.ts +0 -11
  88. package/build/utils/volume-utils.d.ts +0 -17
  89. package/build/utils/window-height.d.ts +0 -2
  90. package/src/components/chrome.tsx +0 -95
  91. package/src/components/overlay.tsx +0 -91
  92. package/src/components/progress-bar.tsx +0 -251
  93. package/src/components/settings.tsx +0 -322
  94. package/src/components/sound.tsx +0 -101
  95. package/src/index.tsx +0 -195
  96. package/src/main.tsx +0 -169
  97. package/src/state-machines/data-source.ts +0 -84
  98. package/src/state-machines/index.ts +0 -5
  99. package/src/state-machines/media-properties.ts +0 -82
  100. package/src/state-machines/media-source.ts +0 -129
  101. package/src/state-machines/media.ts +0 -255
  102. package/src/state-machines/subtitles.ts +0 -285
  103. package/src/state-machines/thumbnails.ts +0 -123
  104. package/src/state-machines/utils.ts +0 -94
  105. package/src/utils/actor-utils.ts +0 -19
  106. package/src/utils/context.ts +0 -23
  107. package/src/utils/fonts.ts +0 -160
  108. package/src/utils/index.ts +0 -229
  109. package/src/utils/languages.ts +0 -262
  110. package/src/utils/mp4box.ts +0 -74
  111. package/src/utils/use-local-storage.ts +0 -30
  112. package/src/utils/use-scrub.ts +0 -40
  113. package/src/utils/volume-utils.ts +0 -39
  114. package/src/utils/window-height.ts +0 -19
  115. package/src/vite-env.d.ts +0 -1
  116. package/tsconfig.json +0 -28
  117. package/tsconfig.node.json +0 -9
  118. /package/{build → dist}/utils/colors.d.ts +0 -0
  119. /package/{build → dist}/utils/fonts.d.ts +0 -0
  120. /package/{build → dist}/utils/time.d.ts +0 -0
  121. /package/src/{assets → lib/assets}/picture-in-picture.svg +0 -0
  122. /package/src/{utils → lib/utils}/colors.ts +0 -0
  123. /package/src/{utils → lib/utils}/time.ts +0 -0
@@ -0,0 +1,341 @@
1
+ /// <reference types="@emotion/react/types/css-prop" />
2
+ import type { DOMAttributes } from 'react'
3
+
4
+ import { useEffect, useMemo, useRef, useState } from 'react'
5
+ import { css } from '@emotion/react'
6
+
7
+ import { usePlayer } from '../player'
8
+ import { useDragValue } from '../hooks/use-drag-value'
9
+ import { fonts } from '../../utils/fonts'
10
+
11
+ const style = css`
12
+ position: relative;
13
+ height: calc(.4 * var(--mp-unit));
14
+
15
+ --thumbnail-width: calc(25 * var(--mp-unit));
16
+ --thumbnail-half: calc(12.5 * var(--mp-unit));
17
+
18
+ transition: transform .2s ease-in-out;
19
+
20
+ margin: 0 12px;
21
+ @media (min-width: 768px) {
22
+ margin: 0 16px;
23
+ }
24
+ @media (min-width: 2560px) {
25
+ margin: 0 24px;
26
+ }
27
+
28
+ user-select: none;
29
+ cursor: pointer;
30
+
31
+ :hover {
32
+ .background-bar {
33
+ transform: scaleY(1.5);
34
+ }
35
+ .loaded {
36
+ transform: scaleY(1.5);
37
+ top: calc(-.1 * var(--mp-unit));
38
+ }
39
+ .play-container {
40
+ transform: scaleY(1.5);
41
+ }
42
+ }
43
+
44
+
45
+ .background-bar {
46
+ position: absolute;
47
+ inset: 0;
48
+ background-color: hsla(0, 100%, 100%, .2);
49
+ }
50
+
51
+ .cursor-time {
52
+ display: flex;
53
+ justify-content: center;
54
+
55
+ text-shadow: 0 0 4px rgba(0, 0, 0, 1);
56
+ ${fonts.bMedium.bold}
57
+
58
+ position: absolute;
59
+ top: calc(-2.5 * var(--mp-unit));
60
+ width: calc(5 * var(--mp-unit));
61
+
62
+ margin-left: calc(-2.5 * var(--mp-unit));
63
+ pointer-events: none;
64
+ }
65
+
66
+ .loaded {
67
+ transform-origin: 0 0;
68
+ position: absolute;
69
+ bottom: 0;
70
+ height: calc(.4 * var(--mp-unit));
71
+ width: 100%;
72
+ .loaded-part {
73
+ transform-origin: 0 0;
74
+ bottom: 0;
75
+ height: calc(.4 * var(--mp-unit));
76
+ width: 100%;
77
+ position: absolute;
78
+ background-color: hsla(0, 100%, 100%, .4);
79
+ }
80
+ }
81
+
82
+ .play-container {
83
+ position: absolute;
84
+ bottom: 0;
85
+ width: 100%;
86
+ height: calc(.4 * var(--mp-unit));
87
+ }
88
+
89
+ .play {
90
+ transform-origin: 0 0;
91
+ background-color: #f03;
92
+ position: absolute;
93
+ bottom: 0;
94
+ height: calc(.4 * var(--mp-unit));
95
+ width: 100%;
96
+ }
97
+
98
+ .padding {
99
+ position: absolute;
100
+ bottom: -7.5px;
101
+ height: calc(2 * var(--mp-unit));
102
+ width: 100%;
103
+ }
104
+
105
+ .thumbnail {
106
+ display: flex;
107
+ justify-content: center;
108
+
109
+ position: absolute;
110
+ top: calc(calc(-3.4375 * var(--mp-unit)) - var(--thumbnail-width) * 9/16);
111
+ height: calc(var(--thumbnail-width) * 9/16);
112
+ width: var(--thumbnail-width);
113
+
114
+ margin-left: calc(var(--thumbnail-half) * -1);
115
+ pointer-events: none;
116
+
117
+ img {
118
+ border-radius: calc(.4 * var(--mp-unit));
119
+ box-shadow: 0 0 calc(1 * var(--mp-unit)) rgba(0, 0, 0, .5);
120
+
121
+ width: 100%;
122
+ height: 100%;
123
+ object-fit: cover;
124
+ }
125
+ }
126
+
127
+ /* a thumb is far wider than a cursor, so the hit strip grows to 44px and the track thickens */
128
+ @media (pointer: coarse) {
129
+ height: calc(.6 * var(--mp-unit));
130
+
131
+ /* press feedback, coarse only: a finger has no hover, so without this the bar gives no sign
132
+ it is being dragged */
133
+ &.dragging {
134
+ .background-bar, .loaded, .play-container {
135
+ transform: scaleY(1.5);
136
+ }
137
+ }
138
+
139
+ .loaded {
140
+ height: calc(.6 * var(--mp-unit));
141
+ .loaded-part {
142
+ height: calc(.6 * var(--mp-unit));
143
+ }
144
+ }
145
+
146
+ .play-container {
147
+ height: calc(.6 * var(--mp-unit));
148
+ }
149
+
150
+ .play {
151
+ height: calc(.6 * var(--mp-unit));
152
+ }
153
+
154
+ /* grows upward into the video: centred, it would hang over the control row and win hit
155
+ testing against taps meant for play, mute and fullscreen */
156
+ .padding {
157
+ height: 44px;
158
+ bottom: 0;
159
+ }
160
+ }
161
+
162
+ /* the clamp only holds the preview inside the bar while the bar is the wider of the two, so
163
+ below that the preview is narrowed instead */
164
+ @media (max-width: 480px) {
165
+ --thumbnail-width: calc(18 * var(--mp-unit));
166
+ --thumbnail-half: calc(9 * var(--mp-unit));
167
+ }
168
+ `
169
+
170
+ export const ProgressBar = () => {
171
+ const player = usePlayer()
172
+ const currentTime = usePlayer((state) => state.currentTime)
173
+ const duration = usePlayer((state) => state.duration)
174
+ const size = usePlayer((state) => state.size)
175
+ const downloadedRanges = usePlayer((state) => state.downloadedRanges)
176
+ const indexes = usePlayer((state) => state.indexes)
177
+ const thumbnails = usePlayer((state) => state.thumbnails)
178
+ const thumbnailAt = usePlayer((state) => state.thumbnailAt)
179
+
180
+ const progressBarRef = useRef<HTMLDivElement>(null)
181
+
182
+ const [seekFraction, setSeekFraction] = useState<number | undefined>(undefined)
183
+ const [progressBarHoverTime, setProgressBarOverTime] = useState<number | undefined>(undefined)
184
+
185
+ // onChange reports a bare fraction, so the device that opened the gesture is recorded on press
186
+ const dragPointerType = useRef<string | undefined>(undefined)
187
+
188
+ const onSeekDrag = (fraction: number) => {
189
+ setSeekFraction(fraction)
190
+ if (dragPointerType.current === 'mouse') return
191
+ setProgressBarOverTime(fraction * duration)
192
+ }
193
+
194
+ const { dragging, handlers } = useDragValue({ ref: progressBarRef, onChange: onSeekDrag })
195
+
196
+ const onDragStart: DOMAttributes<HTMLDivElement>['onPointerDown'] = (ev) => {
197
+ dragPointerType.current = ev.pointerType
198
+ handlers.onPointerDown(ev)
199
+ }
200
+
201
+ const onDragEnd: DOMAttributes<HTMLDivElement>['onPointerUp'] = (ev) => {
202
+ handlers.onPointerUp(ev)
203
+ // a lifted finger leaves nothing over the bar, so the preview it opened closes with it
204
+ if (ev.pointerType === 'mouse') return
205
+ setProgressBarOverTime(undefined)
206
+ }
207
+
208
+ // offsetX is relative to whichever child is under the pointer, and a captured pointer has none
209
+ const timeAtClientX = (clientX: number) => {
210
+ if (!progressBarRef.current) return undefined
211
+ const { left, right } = progressBarRef.current.getBoundingClientRect()
212
+ const fraction = Math.min(Math.max((clientX - left) / (right - left), 0), 1)
213
+ return fraction * duration
214
+ }
215
+
216
+ const onProgressBarOver: DOMAttributes<HTMLDivElement>['onMouseMove'] = (ev) => {
217
+ setProgressBarOverTime(timeAtClientX(ev.clientX))
218
+ }
219
+
220
+ const hideProgressBarTime = () => {
221
+ if (!progressBarRef.current) return
222
+ setProgressBarOverTime(undefined)
223
+ }
224
+
225
+ // duration is 0 until metadata lands, which is never a divisor
226
+ const timePercentage = (time: number) => duration ? (time / duration) * 100 : 0
227
+
228
+ // download percentage is not time percentage: headers and fonts can run to tens of megabytes
229
+ const loadedParts = useMemo(() =>
230
+ downloadedRanges
231
+ ?.map((range, i) => {
232
+ if (!size || !duration) return null
233
+ const matchingIndexes = indexes.filter(index => range.startByteOffset <= index.pos && index.pos <= range.endByteOffset)
234
+ const firstIndex = matchingIndexes.at(0)
235
+ const lastIndex = matchingIndexes.at(-1)
236
+ if (!firstIndex || !lastIndex) return null
237
+ const start = firstIndex.timestamp / duration
238
+ const end = lastIndex.timestamp / duration
239
+ const rangeDuration = Number((end - start).toFixed(2)) // to prevent cases like `0.9995140832939585`
240
+ const left = start * 100
241
+ return (
242
+ <div key={i} className="loaded-part" style={{ transform: `scaleX(${rangeDuration})`, marginLeft: `${left}%` }}></div>
243
+ )
244
+ })
245
+ ?? [],
246
+ [duration, indexes.length, downloadedRanges?.map(({ startByteOffset, endByteOffset }) => `${startByteOffset}/${endByteOffset}`).join(',')]
247
+ )
248
+
249
+ const cusorTimeString = useMemo(() => {
250
+ if (!progressBarHoverTime || progressBarHoverTime < 0) return undefined
251
+ const hours = Math.floor(progressBarHoverTime! / 3600)
252
+ const minutes = Math.floor((progressBarHoverTime! - hours * 3600) / 60)
253
+ const seconds = Math.floor(progressBarHoverTime! - hours * 3600 - minutes * 60)
254
+ const hoursString =
255
+ hours > 0
256
+ ? `${hours}:`
257
+ : ''
258
+ return `${hoursString}${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`
259
+ }, [progressBarHoverTime])
260
+
261
+ useEffect(() => {
262
+ if (seekFraction === undefined || !duration) return
263
+ const timestamp = seekFraction * duration
264
+ player.seek(timestamp)
265
+ }, [player, seekFraction, duration])
266
+
267
+ const scaleX = useMemo(() => {
268
+ return !duration || typeof currentTime !== 'number'
269
+ ? 0
270
+ : currentTime / duration
271
+ }, [duration, currentTime])
272
+
273
+ // an empty url is a gap sentinel, so it renders nothing
274
+ const thumbnail = useMemo(() => {
275
+ if (!progressBarHoverTime) return undefined
276
+ if (thumbnailAt) return thumbnailAt(progressBarHoverTime)
277
+ if (!thumbnails.length) return undefined
278
+ return (
279
+ thumbnails
280
+ .find(({ startTime, endTime }) =>
281
+ startTime <= progressBarHoverTime && progressBarHoverTime < endTime
282
+ )
283
+ )
284
+ }, [thumbnails, thumbnailAt, progressBarHoverTime])
285
+
286
+ return (
287
+ <div
288
+ css={style}
289
+ ref={progressBarRef}
290
+ className={dragging ? 'progress-bar dragging' : 'progress-bar'}
291
+ onMouseMove={onProgressBarOver}
292
+ onMouseOut={hideProgressBarTime}
293
+ >
294
+ <div className="background-bar" />
295
+ {
296
+ progressBarHoverTime
297
+ ? (
298
+ <div
299
+ className="cursor-time"
300
+ style={{ left: `clamp(calc(1.8 * var(--mp-unit)), ${timePercentage(progressBarHoverTime)}%, calc(100% - calc(1.8 * var(--mp-unit))))` }}
301
+ >
302
+ {cusorTimeString}
303
+ </div>
304
+ )
305
+ : undefined
306
+ }
307
+ <div className="progress"></div>
308
+ {/* bar showing the currently loaded progress */}
309
+ <div className="loaded">
310
+ {loadedParts}
311
+ </div>
312
+ {/* bar to show when hovering to potentially seek */}
313
+ <div className="hover"></div>
314
+ {/* bar displaying the current playback progress */}
315
+ <div className='play-container'>
316
+ <div className="play" style={{ transform: `scaleX(${scaleX})` }}></div>
317
+ </div>
318
+ <div className="chapters"></div>
319
+ <div className="scrubber"></div>
320
+ <div
321
+ className="padding"
322
+ {...handlers}
323
+ onPointerDown={onDragStart}
324
+ onPointerUp={onDragEnd}
325
+ onPointerCancel={onDragEnd}
326
+ ></div>
327
+ <div
328
+ className="thumbnail"
329
+ style={{ left: `clamp(var(--thumbnail-half), ${timePercentage(progressBarHoverTime ?? 1)}%, calc(100% - var(--thumbnail-half)))` }}
330
+ >
331
+ {
332
+ thumbnail?.url
333
+ ? <img src={thumbnail.url}/>
334
+ : undefined
335
+ }
336
+ </div>
337
+ </div>
338
+ )
339
+ }
340
+
341
+ export default ProgressBar