@stepincto/expo-video 1.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.
Files changed (180) hide show
  1. package/README.md +45 -0
  2. package/android/build.gradle +32 -0
  3. package/android/src/main/AndroidManifest.xml +20 -0
  4. package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
  5. package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
  6. package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
  7. package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
  8. package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
  9. package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
  10. package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
  11. package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
  12. package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
  13. package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
  14. package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
  15. package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
  16. package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
  17. package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
  18. package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
  19. package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
  20. package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
  21. package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
  22. package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
  23. package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
  24. package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
  25. package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
  26. package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
  27. package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
  28. package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
  29. package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
  30. package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
  31. package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
  32. package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
  33. package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
  34. package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
  35. package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
  36. package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
  37. package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
  38. package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
  39. package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
  40. package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
  41. package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
  42. package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
  43. package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
  44. package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
  45. package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
  46. package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
  47. package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
  48. package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
  49. package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
  50. package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
  51. package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
  52. package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
  53. package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
  54. package/android/src/main/res/layout/surface_player_view.xml +7 -0
  55. package/android/src/main/res/layout/texture_player_view.xml +7 -0
  56. package/android/src/main/res/values/styles.xml +9 -0
  57. package/app.plugin.js +1 -0
  58. package/build/NativeVideoModule.d.ts +16 -0
  59. package/build/NativeVideoModule.d.ts.map +1 -0
  60. package/build/NativeVideoModule.js +3 -0
  61. package/build/NativeVideoModule.js.map +1 -0
  62. package/build/NativeVideoModule.web.d.ts +3 -0
  63. package/build/NativeVideoModule.web.d.ts.map +1 -0
  64. package/build/NativeVideoModule.web.js +2 -0
  65. package/build/NativeVideoModule.web.js.map +1 -0
  66. package/build/NativeVideoView.d.ts +4 -0
  67. package/build/NativeVideoView.d.ts.map +1 -0
  68. package/build/NativeVideoView.js +6 -0
  69. package/build/NativeVideoView.js.map +1 -0
  70. package/build/VideoModule.d.ts +38 -0
  71. package/build/VideoModule.d.ts.map +1 -0
  72. package/build/VideoModule.js +53 -0
  73. package/build/VideoModule.js.map +1 -0
  74. package/build/VideoPlayer.d.ts +15 -0
  75. package/build/VideoPlayer.d.ts.map +1 -0
  76. package/build/VideoPlayer.js +52 -0
  77. package/build/VideoPlayer.js.map +1 -0
  78. package/build/VideoPlayer.types.d.ts +532 -0
  79. package/build/VideoPlayer.types.d.ts.map +1 -0
  80. package/build/VideoPlayer.types.js +2 -0
  81. package/build/VideoPlayer.types.js.map +1 -0
  82. package/build/VideoPlayer.web.d.ts +75 -0
  83. package/build/VideoPlayer.web.d.ts.map +1 -0
  84. package/build/VideoPlayer.web.js +376 -0
  85. package/build/VideoPlayer.web.js.map +1 -0
  86. package/build/VideoPlayerEvents.types.d.ts +262 -0
  87. package/build/VideoPlayerEvents.types.d.ts.map +1 -0
  88. package/build/VideoPlayerEvents.types.js +2 -0
  89. package/build/VideoPlayerEvents.types.js.map +1 -0
  90. package/build/VideoThumbnail.d.ts +29 -0
  91. package/build/VideoThumbnail.d.ts.map +1 -0
  92. package/build/VideoThumbnail.js +3 -0
  93. package/build/VideoThumbnail.js.map +1 -0
  94. package/build/VideoView.d.ts +44 -0
  95. package/build/VideoView.d.ts.map +1 -0
  96. package/build/VideoView.js +76 -0
  97. package/build/VideoView.js.map +1 -0
  98. package/build/VideoView.types.d.ts +147 -0
  99. package/build/VideoView.types.d.ts.map +1 -0
  100. package/build/VideoView.types.js +2 -0
  101. package/build/VideoView.types.js.map +1 -0
  102. package/build/VideoView.web.d.ts +9 -0
  103. package/build/VideoView.web.d.ts.map +1 -0
  104. package/build/VideoView.web.js +180 -0
  105. package/build/VideoView.web.js.map +1 -0
  106. package/build/index.d.ts +9 -0
  107. package/build/index.d.ts.map +1 -0
  108. package/build/index.js +7 -0
  109. package/build/index.js.map +1 -0
  110. package/build/resolveAssetSource.d.ts +3 -0
  111. package/build/resolveAssetSource.d.ts.map +1 -0
  112. package/build/resolveAssetSource.js +3 -0
  113. package/build/resolveAssetSource.js.map +1 -0
  114. package/build/resolveAssetSource.web.d.ts +4 -0
  115. package/build/resolveAssetSource.web.d.ts.map +1 -0
  116. package/build/resolveAssetSource.web.js +16 -0
  117. package/build/resolveAssetSource.web.js.map +1 -0
  118. package/expo-module.config.json +9 -0
  119. package/ios/Cache/CachableRequest.swift +44 -0
  120. package/ios/Cache/CachedResource.swift +97 -0
  121. package/ios/Cache/CachingHelpers.swift +92 -0
  122. package/ios/Cache/MediaFileHandle.swift +94 -0
  123. package/ios/Cache/MediaInfo.swift +147 -0
  124. package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
  125. package/ios/Cache/SynchronizedHashTable.swift +23 -0
  126. package/ios/Cache/VideoCacheManager.swift +338 -0
  127. package/ios/ContentKeyDelegate.swift +214 -0
  128. package/ios/ContentKeyManager.swift +21 -0
  129. package/ios/Enums/AudioMixingMode.swift +37 -0
  130. package/ios/Enums/ContentType.swift +12 -0
  131. package/ios/Enums/DRMType.swift +20 -0
  132. package/ios/Enums/PlayerStatus.swift +10 -0
  133. package/ios/Enums/VideoContentFit.swift +39 -0
  134. package/ios/ExpoVideo.podspec +29 -0
  135. package/ios/NowPlayingManager.swift +296 -0
  136. package/ios/Records/BufferOptions.swift +12 -0
  137. package/ios/Records/DRMOptions.swift +24 -0
  138. package/ios/Records/PlaybackError.swift +10 -0
  139. package/ios/Records/Tracks.swift +176 -0
  140. package/ios/Records/VideoEventPayloads.swift +76 -0
  141. package/ios/Records/VideoMetadata.swift +16 -0
  142. package/ios/Records/VideoSize.swift +15 -0
  143. package/ios/Records/VideoSource.swift +25 -0
  144. package/ios/Thumbnails/VideoThumbnail.swift +27 -0
  145. package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
  146. package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
  147. package/ios/VideoAsset.swift +123 -0
  148. package/ios/VideoExceptions.swift +53 -0
  149. package/ios/VideoItem.swift +11 -0
  150. package/ios/VideoManager.swift +140 -0
  151. package/ios/VideoModule.swift +383 -0
  152. package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
  153. package/ios/VideoPlayer.swift +435 -0
  154. package/ios/VideoPlayerAudioTracks.swift +72 -0
  155. package/ios/VideoPlayerItem.swift +97 -0
  156. package/ios/VideoPlayerObserver.swift +523 -0
  157. package/ios/VideoPlayerSubtitles.swift +71 -0
  158. package/ios/VideoSourceLoader.swift +89 -0
  159. package/ios/VideoSourceLoaderListener.swift +34 -0
  160. package/ios/VideoView.swift +224 -0
  161. package/package.json +59 -0
  162. package/plugin/build/tsconfig.tsbuildinfo +1 -0
  163. package/plugin/build/withExpoVideo.d.ts +7 -0
  164. package/plugin/build/withExpoVideo.js +38 -0
  165. package/src/NativeVideoModule.ts +20 -0
  166. package/src/NativeVideoModule.web.ts +1 -0
  167. package/src/NativeVideoView.ts +8 -0
  168. package/src/VideoModule.ts +59 -0
  169. package/src/VideoPlayer.tsx +67 -0
  170. package/src/VideoPlayer.types.ts +613 -0
  171. package/src/VideoPlayer.web.tsx +451 -0
  172. package/src/VideoPlayerEvents.types.ts +313 -0
  173. package/src/VideoThumbnail.ts +31 -0
  174. package/src/VideoView.tsx +86 -0
  175. package/src/VideoView.types.ts +165 -0
  176. package/src/VideoView.web.tsx +214 -0
  177. package/src/index.ts +46 -0
  178. package/src/resolveAssetSource.ts +2 -0
  179. package/src/resolveAssetSource.web.ts +17 -0
  180. package/src/ts-declarations/react-native-assets.d.ts +1 -0
@@ -0,0 +1,451 @@
1
+ import { useMemo } from 'react';
2
+
3
+ import type {
4
+ BufferOptions,
5
+ PlayerError,
6
+ VideoPlayerStatus,
7
+ VideoSource,
8
+ VideoPlayer,
9
+ SubtitleTrack,
10
+ AudioMixingMode,
11
+ VideoTrack,
12
+ AudioTrack,
13
+ } from './VideoPlayer.types';
14
+ import type { VideoPlayerEvents } from './VideoPlayerEvents.types';
15
+ import { VideoThumbnail } from './VideoThumbnail';
16
+ import resolveAssetSource from './resolveAssetSource';
17
+
18
+ export function useVideoPlayer(
19
+ source: VideoSource,
20
+ setup?: (player: VideoPlayer) => void
21
+ ): VideoPlayer {
22
+ const parsedSource = typeof source === 'string' ? { uri: source } : source;
23
+
24
+ return useMemo(() => {
25
+ const player = new VideoPlayerWeb(parsedSource);
26
+ setup?.(player);
27
+ return player;
28
+ }, [JSON.stringify(source)]);
29
+ }
30
+
31
+ export function getSourceUri(source: VideoSource): string | null {
32
+ if (typeof source === 'string') {
33
+ return source;
34
+ }
35
+ if (typeof source === 'number') {
36
+ return resolveAssetSource(source)?.uri ?? null;
37
+ }
38
+ if (typeof source?.assetId === 'number' && !source?.uri) {
39
+ return resolveAssetSource(source.assetId)?.uri ?? null;
40
+ }
41
+
42
+ return source?.uri ?? null;
43
+ }
44
+
45
+ export function createVideoPlayer(source: VideoSource): VideoPlayer {
46
+ const parsedSource = typeof source === 'string' ? { uri: source } : source;
47
+
48
+ return new VideoPlayerWeb(parsedSource);
49
+ }
50
+
51
+ export default class VideoPlayerWeb
52
+ extends globalThis.expo.SharedObject<VideoPlayerEvents>
53
+ implements VideoPlayer
54
+ {
55
+ constructor(source: VideoSource) {
56
+ super();
57
+ this.src = source;
58
+ }
59
+
60
+ src: VideoSource = null;
61
+ previousSrc: VideoSource = null;
62
+ _mountedVideos: Set<HTMLVideoElement> = new Set();
63
+ _audioNodes: Set<MediaElementAudioSourceNode> = new Set();
64
+ playing: boolean = false;
65
+ _muted: boolean = false;
66
+ _volume: number = 1;
67
+ _loop: boolean = false;
68
+ _playbackRate: number = 1.0;
69
+ _preservesPitch: boolean = true;
70
+ _status: VideoPlayerStatus = 'idle';
71
+ _error: PlayerError | null = null;
72
+ _timeUpdateLoop: number | null = null;
73
+ _timeUpdateEventInterval: number = 0;
74
+ audioMixingMode: AudioMixingMode = 'auto'; // Not supported on web. Dummy to match the interface.
75
+ allowsExternalPlayback: boolean = false; // Not supported on web. Dummy to match the interface.
76
+ staysActiveInBackground: boolean = false; // Not supported on web. Dummy to match the interface.
77
+ showNowPlayingNotification: boolean = false; // Not supported on web. Dummy to match the interface.
78
+ currentLiveTimestamp: number | null = null; // Not supported on web. Dummy to match the interface.
79
+ currentOffsetFromLive: number | null = null; // Not supported on web. Dummy to match the interface.
80
+ targetOffsetFromLive: number = 0; // Not supported on web. Dummy to match the interface.
81
+ bufferOptions: BufferOptions = {} as BufferOptions; // Not supported on web. Dummy to match the interface.
82
+ subtitleTrack: SubtitleTrack | null = null; // Embedded subtitles are not supported by the html web player. Dummy to match the interface.
83
+ availableSubtitleTracks: SubtitleTrack[] = []; // Embedded subtitles are not supported by the html web player. Dummy to match the interface.
84
+ audioTrack: AudioTrack | null = null; // Not supported on web. Dummy to match the interface.
85
+ availableAudioTracks: AudioTrack[] = []; // Not supported on web. Dummy to match the interface.
86
+ videoTrack: VideoTrack | null = null; // Not supported on web. Dummy to match the interface.
87
+ availableVideoTracks: VideoTrack[] = []; // Not supported on web. Dummy to match the interface.
88
+
89
+ set muted(value: boolean) {
90
+ this._mountedVideos.forEach((video) => {
91
+ video.muted = value;
92
+ });
93
+ this._muted = value;
94
+ }
95
+
96
+ get muted(): boolean {
97
+ return this._muted;
98
+ }
99
+
100
+ set playbackRate(value: number) {
101
+ this._mountedVideos.forEach((video) => {
102
+ video.playbackRate = value;
103
+ });
104
+ this._playbackRate = value;
105
+ }
106
+
107
+ get playbackRate(): number {
108
+ return this._playbackRate;
109
+ }
110
+
111
+ get isLive(): boolean {
112
+ return [...this._mountedVideos][0]?.duration === Infinity;
113
+ }
114
+
115
+ set volume(value: number) {
116
+ this._mountedVideos.forEach((video) => {
117
+ video.volume = value;
118
+ });
119
+ this._volume = value;
120
+ }
121
+
122
+ get volume(): number {
123
+ return this._volume;
124
+ }
125
+
126
+ set loop(value: boolean) {
127
+ this._mountedVideos.forEach((video) => {
128
+ video.loop = value;
129
+ });
130
+ this._loop = value;
131
+ }
132
+
133
+ get loop(): boolean {
134
+ return this._loop;
135
+ }
136
+
137
+ get currentTime(): number {
138
+ // All videos should be synchronized, so we return the position of the first video.
139
+ return [...this._mountedVideos][0]?.currentTime ?? 0;
140
+ }
141
+
142
+ set currentTime(value: number) {
143
+ this._mountedVideos.forEach((video) => {
144
+ video.currentTime = value;
145
+ });
146
+ }
147
+
148
+ get duration(): number {
149
+ // All videos should have the same duration, so we return the duration of the first video.
150
+ return [...this._mountedVideos][0]?.duration ?? 0;
151
+ }
152
+
153
+ get preservesPitch(): boolean {
154
+ return this._preservesPitch;
155
+ }
156
+
157
+ set preservesPitch(value: boolean) {
158
+ this._mountedVideos.forEach((video) => {
159
+ video.preservesPitch = value;
160
+ });
161
+ this._preservesPitch = value;
162
+ }
163
+
164
+ get timeUpdateEventInterval(): number {
165
+ return this._timeUpdateEventInterval;
166
+ }
167
+ set timeUpdateEventInterval(value: number) {
168
+ this._timeUpdateEventInterval = value;
169
+ if (this._timeUpdateLoop) {
170
+ clearInterval(this._timeUpdateLoop);
171
+ }
172
+ if (value > 0) {
173
+ // Emit the first event immediately like on other platforms
174
+ this.emit('timeUpdate', {
175
+ currentTime: this.currentTime,
176
+ currentLiveTimestamp: null,
177
+ currentOffsetFromLive: null,
178
+ bufferedPosition: this.bufferedPosition,
179
+ });
180
+
181
+ this._timeUpdateLoop = setInterval(() => {
182
+ this.emit('timeUpdate', {
183
+ currentTime: this.currentTime,
184
+ currentLiveTimestamp: null,
185
+ currentOffsetFromLive: null,
186
+ bufferedPosition: this.bufferedPosition,
187
+ });
188
+ }, value * 1000);
189
+ }
190
+ }
191
+
192
+ get status(): VideoPlayerStatus {
193
+ return this._status;
194
+ }
195
+
196
+ get bufferedPosition(): number {
197
+ if (this._mountedVideos.size === 0 || this.status === 'error') {
198
+ return -1;
199
+ }
200
+ const buffered = [...this._mountedVideos][0]?.buffered;
201
+ for (let i = 0; i < buffered.length; i++) {
202
+ if (buffered.start(i) <= this.currentTime && buffered.end(i) >= this.currentTime) {
203
+ return buffered.end(i);
204
+ }
205
+ }
206
+ return 0;
207
+ }
208
+
209
+ private set status(value: VideoPlayerStatus) {
210
+ if (this._status === value) return;
211
+
212
+ if (value === 'error' && this._error) {
213
+ this.emit('statusChange', {
214
+ status: value,
215
+ oldStatus: this._status,
216
+ error: this._error,
217
+ });
218
+ } else {
219
+ this.emit('statusChange', {
220
+ status: value,
221
+ oldStatus: this._status,
222
+ });
223
+ this._error = null;
224
+ }
225
+ this._status = value;
226
+ }
227
+
228
+ mountVideoView(video: HTMLVideoElement) {
229
+ // The video will be the first video, it should inherit the properties set in the setup() function
230
+ if (this._mountedVideos.size === 0) {
231
+ video.preservesPitch = this._preservesPitch;
232
+ video.loop = this._loop;
233
+ video.volume = this._volume;
234
+ video.muted = this._muted;
235
+ video.playbackRate = this._playbackRate;
236
+ }
237
+ this._mountedVideos.add(video);
238
+ this._addListeners(video);
239
+ this._synchronizeWithFirstVideo(video);
240
+ }
241
+
242
+ unmountVideoView(video: HTMLVideoElement) {
243
+ this._mountedVideos.delete(video);
244
+ }
245
+
246
+ mountAudioNode(
247
+ audioContext: AudioContext,
248
+ zeroGainNode: GainNode,
249
+ audioSourceNode: MediaElementAudioSourceNode
250
+ ): void {
251
+ if (!audioContext || !zeroGainNode) return;
252
+
253
+ this._audioNodes.add(audioSourceNode);
254
+ // First mounted video should be connected to the audio context. All other videos have to be muted.
255
+ if (this._audioNodes.size === 1) {
256
+ audioSourceNode.connect(audioContext.destination);
257
+ } else {
258
+ audioSourceNode.connect(zeroGainNode);
259
+ }
260
+ }
261
+
262
+ unmountAudioNode(
263
+ video: HTMLVideoElement,
264
+ audioContext: AudioContext,
265
+ audioSourceNode: MediaElementAudioSourceNode
266
+ ) {
267
+ const mountedVideos = [...this._mountedVideos];
268
+ const videoPlayingAudio = mountedVideos[0];
269
+ this._audioNodes.delete(audioSourceNode);
270
+ audioSourceNode.disconnect();
271
+
272
+ // If video playing audio has been removed, select a new video to be the audio player by disconnecting it from the mute node.
273
+ if (videoPlayingAudio === video && this._audioNodes.size > 0 && audioContext) {
274
+ const newMainAudioSource = [...this._audioNodes][0];
275
+ newMainAudioSource.disconnect();
276
+ newMainAudioSource.connect(audioContext.destination);
277
+ }
278
+ }
279
+
280
+ play(): void {
281
+ this._mountedVideos.forEach((video) => {
282
+ video.play();
283
+ });
284
+ }
285
+
286
+ pause(): void {
287
+ this._mountedVideos.forEach((video) => {
288
+ video.pause();
289
+ });
290
+ }
291
+
292
+ replace(source: VideoSource): void {
293
+ this._mountedVideos.forEach((video) => {
294
+ const uri = getSourceUri(source);
295
+ video.pause();
296
+ if (uri) {
297
+ video.setAttribute('src', uri);
298
+ video.load();
299
+ video.play();
300
+ } else {
301
+ video.removeAttribute('src');
302
+ video.load();
303
+ }
304
+ });
305
+ // TODO @behenate: this won't work when we add support for playlists
306
+ this.previousSrc = this.src;
307
+ this.src = source;
308
+ this.playing = true;
309
+ }
310
+
311
+ // The HTML5 player already offloads loading of the asset onto a different thread so we can keep the same
312
+ // implementation until `replace` is deprecated and removed.
313
+ async replaceAsync(source: VideoSource): Promise<void> {
314
+ return this.replace(source);
315
+ }
316
+
317
+ seekBy(seconds: number): void {
318
+ this._mountedVideos.forEach((video) => {
319
+ video.currentTime += seconds;
320
+ });
321
+ }
322
+
323
+ replay(): void {
324
+ this._mountedVideos.forEach((video) => {
325
+ video.currentTime = 0;
326
+ video.play();
327
+ });
328
+ this.playing = true;
329
+ }
330
+
331
+ generateThumbnailsAsync(times: number | number[]): Promise<VideoThumbnail[]> {
332
+ throw new Error('Generating video thumbnails is not supported on Web yet');
333
+ }
334
+
335
+ _synchronizeWithFirstVideo(video: HTMLVideoElement): void {
336
+ const firstVideo = [...this._mountedVideos][0];
337
+ if (!firstVideo) return;
338
+
339
+ if (firstVideo.paused) {
340
+ video.pause();
341
+ } else {
342
+ video.play();
343
+ }
344
+ video.currentTime = firstVideo.currentTime;
345
+ video.volume = firstVideo.volume;
346
+ video.muted = firstVideo.muted;
347
+ video.playbackRate = firstVideo.playbackRate;
348
+ }
349
+
350
+ /**
351
+ * If there are multiple mounted videos, all of them will emit an event, as they are synchronised.
352
+ * We want to avoid this, so we only emit the event if it came from the first video.
353
+ */
354
+ _emitOnce<EventName extends keyof VideoPlayerEvents>(
355
+ eventSource: HTMLVideoElement,
356
+ eventName: EventName,
357
+ ...args: Parameters<VideoPlayerEvents[EventName]>
358
+ ): void {
359
+ const mountedVideos = [...this._mountedVideos];
360
+ if (mountedVideos[0] === eventSource) {
361
+ this.emit(eventName, ...args);
362
+ }
363
+ }
364
+
365
+ _addListeners(video: HTMLVideoElement): void {
366
+ video.onplay = () => {
367
+ this._emitOnce(video, 'playingChange', {
368
+ isPlaying: true,
369
+ oldIsPlaying: this.playing,
370
+ });
371
+ this.playing = true;
372
+ this._mountedVideos.forEach((mountedVideo) => {
373
+ mountedVideo.play();
374
+ });
375
+ };
376
+
377
+ video.onpause = () => {
378
+ this._emitOnce(video, 'playingChange', {
379
+ isPlaying: false,
380
+ oldIsPlaying: this.playing,
381
+ });
382
+ this.playing = false;
383
+ this._mountedVideos.forEach((mountedVideo) => {
384
+ mountedVideo.pause();
385
+ });
386
+ };
387
+
388
+ video.onvolumechange = () => {
389
+ this._emitOnce(video, 'volumeChange', { volume: video.volume, oldVolume: this.volume });
390
+ this._emitOnce(video, 'mutedChange', { muted: video.muted, oldMuted: this.muted });
391
+ this.volume = video.volume;
392
+ this.muted = video.muted;
393
+ };
394
+
395
+ video.onseeking = () => {
396
+ this._mountedVideos.forEach((mountedVideo) => {
397
+ if (mountedVideo === video || mountedVideo.currentTime === video.currentTime) return;
398
+ mountedVideo.currentTime = video.currentTime;
399
+ });
400
+ };
401
+
402
+ video.onseeked = () => {
403
+ this._mountedVideos.forEach((mountedVideo) => {
404
+ if (mountedVideo === video || mountedVideo.currentTime === video.currentTime) return;
405
+ mountedVideo.currentTime = video.currentTime;
406
+ });
407
+ };
408
+
409
+ video.onratechange = () => {
410
+ this._emitOnce(video, 'playbackRateChange', {
411
+ playbackRate: video.playbackRate,
412
+ oldPlaybackRate: this.playbackRate,
413
+ });
414
+ this._mountedVideos.forEach((mountedVideo) => {
415
+ if (mountedVideo.playbackRate === video.playbackRate) return;
416
+ this._playbackRate = video.playbackRate;
417
+ mountedVideo.playbackRate = video.playbackRate;
418
+ });
419
+ this._playbackRate = video.playbackRate;
420
+ };
421
+
422
+ video.onerror = () => {
423
+ this._error = {
424
+ message: video.error?.message ?? 'Unknown player error',
425
+ };
426
+ this.status = 'error';
427
+ };
428
+
429
+ video.oncanplay = () => {
430
+ const allCanPlay = [...this._mountedVideos].reduce((previousValue, video) => {
431
+ return previousValue && video.readyState >= 3;
432
+ }, true);
433
+ if (!allCanPlay) return;
434
+
435
+ this.status = 'readyToPlay';
436
+ };
437
+
438
+ video.onwaiting = () => {
439
+ if (this._status === 'loading') return;
440
+ this.status = 'loading';
441
+ };
442
+
443
+ video.onended = () => {
444
+ this._emitOnce(video, 'playToEnd');
445
+ };
446
+
447
+ video.onloadstart = () => {
448
+ this._emitOnce(video, 'sourceChange', { source: this.src, oldSource: this.previousSrc });
449
+ };
450
+ }
451
+ }