@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.
- package/README.md +45 -0
- package/android/build.gradle +32 -0
- package/android/src/main/AndroidManifest.xml +20 -0
- package/android/src/main/java/expo/modules/video/AudioFocusManager.kt +241 -0
- package/android/src/main/java/expo/modules/video/FullscreenPlayerActivity.kt +145 -0
- package/android/src/main/java/expo/modules/video/IntervalUpdateClock.kt +54 -0
- package/android/src/main/java/expo/modules/video/MediaMetadataRetriever.kt +89 -0
- package/android/src/main/java/expo/modules/video/PictureInPictureHelperFragment.kt +26 -0
- package/android/src/main/java/expo/modules/video/PlayerViewExtension.kt +36 -0
- package/android/src/main/java/expo/modules/video/VideoCache.kt +104 -0
- package/android/src/main/java/expo/modules/video/VideoExceptions.kt +34 -0
- package/android/src/main/java/expo/modules/video/VideoManager.kt +133 -0
- package/android/src/main/java/expo/modules/video/VideoModule.kt +414 -0
- package/android/src/main/java/expo/modules/video/VideoThumbnail.kt +20 -0
- package/android/src/main/java/expo/modules/video/VideoView.kt +367 -0
- package/android/src/main/java/expo/modules/video/delegates/IgnoreSameSet.kt +24 -0
- package/android/src/main/java/expo/modules/video/drawing/OutlineProvider.kt +217 -0
- package/android/src/main/java/expo/modules/video/enums/AudioMixingMode.kt +20 -0
- package/android/src/main/java/expo/modules/video/enums/ContentFit.kt +19 -0
- package/android/src/main/java/expo/modules/video/enums/ContentType.kt +22 -0
- package/android/src/main/java/expo/modules/video/enums/DRMType.kt +26 -0
- package/android/src/main/java/expo/modules/video/enums/PlayerStatus.kt +10 -0
- package/android/src/main/java/expo/modules/video/playbackService/ExpoVideoPlaybackService.kt +184 -0
- package/android/src/main/java/expo/modules/video/playbackService/PlaybackServiceConnection.kt +39 -0
- package/android/src/main/java/expo/modules/video/playbackService/VideoMediaSessionCallback.kt +47 -0
- package/android/src/main/java/expo/modules/video/player/FirstFrameEventGenerator.kt +93 -0
- package/android/src/main/java/expo/modules/video/player/PlayerEvent.kt +164 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayer.kt +460 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerAudioTracks.kt +125 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerListener.kt +32 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerLoadControl.kt +525 -0
- package/android/src/main/java/expo/modules/video/player/VideoPlayerSubtitles.kt +125 -0
- package/android/src/main/java/expo/modules/video/records/BufferOptions.kt +15 -0
- package/android/src/main/java/expo/modules/video/records/DRMOptions.kt +25 -0
- package/android/src/main/java/expo/modules/video/records/PlaybackError.kt +19 -0
- package/android/src/main/java/expo/modules/video/records/Tracks.kt +81 -0
- package/android/src/main/java/expo/modules/video/records/VideoEventPayloads.kt +79 -0
- package/android/src/main/java/expo/modules/video/records/VideoMetadata.kt +12 -0
- package/android/src/main/java/expo/modules/video/records/VideoSize.kt +14 -0
- package/android/src/main/java/expo/modules/video/records/VideoSource.kt +104 -0
- package/android/src/main/java/expo/modules/video/records/VideoThumbnailOptions.kt +24 -0
- package/android/src/main/java/expo/modules/video/utils/DataSourceUtils.kt +75 -0
- package/android/src/main/java/expo/modules/video/utils/EventDispatcherUtils.kt +43 -0
- package/android/src/main/java/expo/modules/video/utils/MutableWeakReference.kt +15 -0
- package/android/src/main/java/expo/modules/video/utils/PictureInPictureUtils.kt +96 -0
- package/android/src/main/java/expo/modules/video/utils/YogaUtils.kt +20 -0
- package/android/src/main/res/drawable/seek_backwards_10s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_15s.xml +25 -0
- package/android/src/main/res/drawable/seek_backwards_5s.xml +25 -0
- package/android/src/main/res/drawable/seek_forwards_10s.xml +30 -0
- package/android/src/main/res/drawable/seek_forwards_15s.xml +31 -0
- package/android/src/main/res/drawable/seek_forwards_5s.xml +30 -0
- package/android/src/main/res/layout/fullscreen_player_activity.xml +16 -0
- package/android/src/main/res/layout/surface_player_view.xml +7 -0
- package/android/src/main/res/layout/texture_player_view.xml +7 -0
- package/android/src/main/res/values/styles.xml +9 -0
- package/app.plugin.js +1 -0
- package/build/NativeVideoModule.d.ts +16 -0
- package/build/NativeVideoModule.d.ts.map +1 -0
- package/build/NativeVideoModule.js +3 -0
- package/build/NativeVideoModule.js.map +1 -0
- package/build/NativeVideoModule.web.d.ts +3 -0
- package/build/NativeVideoModule.web.d.ts.map +1 -0
- package/build/NativeVideoModule.web.js +2 -0
- package/build/NativeVideoModule.web.js.map +1 -0
- package/build/NativeVideoView.d.ts +4 -0
- package/build/NativeVideoView.d.ts.map +1 -0
- package/build/NativeVideoView.js +6 -0
- package/build/NativeVideoView.js.map +1 -0
- package/build/VideoModule.d.ts +38 -0
- package/build/VideoModule.d.ts.map +1 -0
- package/build/VideoModule.js +53 -0
- package/build/VideoModule.js.map +1 -0
- package/build/VideoPlayer.d.ts +15 -0
- package/build/VideoPlayer.d.ts.map +1 -0
- package/build/VideoPlayer.js +52 -0
- package/build/VideoPlayer.js.map +1 -0
- package/build/VideoPlayer.types.d.ts +532 -0
- package/build/VideoPlayer.types.d.ts.map +1 -0
- package/build/VideoPlayer.types.js +2 -0
- package/build/VideoPlayer.types.js.map +1 -0
- package/build/VideoPlayer.web.d.ts +75 -0
- package/build/VideoPlayer.web.d.ts.map +1 -0
- package/build/VideoPlayer.web.js +376 -0
- package/build/VideoPlayer.web.js.map +1 -0
- package/build/VideoPlayerEvents.types.d.ts +262 -0
- package/build/VideoPlayerEvents.types.d.ts.map +1 -0
- package/build/VideoPlayerEvents.types.js +2 -0
- package/build/VideoPlayerEvents.types.js.map +1 -0
- package/build/VideoThumbnail.d.ts +29 -0
- package/build/VideoThumbnail.d.ts.map +1 -0
- package/build/VideoThumbnail.js +3 -0
- package/build/VideoThumbnail.js.map +1 -0
- package/build/VideoView.d.ts +44 -0
- package/build/VideoView.d.ts.map +1 -0
- package/build/VideoView.js +76 -0
- package/build/VideoView.js.map +1 -0
- package/build/VideoView.types.d.ts +147 -0
- package/build/VideoView.types.d.ts.map +1 -0
- package/build/VideoView.types.js +2 -0
- package/build/VideoView.types.js.map +1 -0
- package/build/VideoView.web.d.ts +9 -0
- package/build/VideoView.web.d.ts.map +1 -0
- package/build/VideoView.web.js +180 -0
- package/build/VideoView.web.js.map +1 -0
- package/build/index.d.ts +9 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +7 -0
- package/build/index.js.map +1 -0
- package/build/resolveAssetSource.d.ts +3 -0
- package/build/resolveAssetSource.d.ts.map +1 -0
- package/build/resolveAssetSource.js +3 -0
- package/build/resolveAssetSource.js.map +1 -0
- package/build/resolveAssetSource.web.d.ts +4 -0
- package/build/resolveAssetSource.web.d.ts.map +1 -0
- package/build/resolveAssetSource.web.js +16 -0
- package/build/resolveAssetSource.web.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/Cache/CachableRequest.swift +44 -0
- package/ios/Cache/CachedResource.swift +97 -0
- package/ios/Cache/CachingHelpers.swift +92 -0
- package/ios/Cache/MediaFileHandle.swift +94 -0
- package/ios/Cache/MediaInfo.swift +147 -0
- package/ios/Cache/ResourceLoaderDelegate.swift +274 -0
- package/ios/Cache/SynchronizedHashTable.swift +23 -0
- package/ios/Cache/VideoCacheManager.swift +338 -0
- package/ios/ContentKeyDelegate.swift +214 -0
- package/ios/ContentKeyManager.swift +21 -0
- package/ios/Enums/AudioMixingMode.swift +37 -0
- package/ios/Enums/ContentType.swift +12 -0
- package/ios/Enums/DRMType.swift +20 -0
- package/ios/Enums/PlayerStatus.swift +10 -0
- package/ios/Enums/VideoContentFit.swift +39 -0
- package/ios/ExpoVideo.podspec +29 -0
- package/ios/NowPlayingManager.swift +296 -0
- package/ios/Records/BufferOptions.swift +12 -0
- package/ios/Records/DRMOptions.swift +24 -0
- package/ios/Records/PlaybackError.swift +10 -0
- package/ios/Records/Tracks.swift +176 -0
- package/ios/Records/VideoEventPayloads.swift +76 -0
- package/ios/Records/VideoMetadata.swift +16 -0
- package/ios/Records/VideoSize.swift +15 -0
- package/ios/Records/VideoSource.swift +25 -0
- package/ios/Thumbnails/VideoThumbnail.swift +27 -0
- package/ios/Thumbnails/VideoThumbnailGenerator.swift +68 -0
- package/ios/Thumbnails/VideoThumbnailOptions.swift +15 -0
- package/ios/VideoAsset.swift +123 -0
- package/ios/VideoExceptions.swift +53 -0
- package/ios/VideoItem.swift +11 -0
- package/ios/VideoManager.swift +140 -0
- package/ios/VideoModule.swift +383 -0
- package/ios/VideoPlayer/DangerousPropertiesStore.swift +19 -0
- package/ios/VideoPlayer.swift +435 -0
- package/ios/VideoPlayerAudioTracks.swift +72 -0
- package/ios/VideoPlayerItem.swift +97 -0
- package/ios/VideoPlayerObserver.swift +523 -0
- package/ios/VideoPlayerSubtitles.swift +71 -0
- package/ios/VideoSourceLoader.swift +89 -0
- package/ios/VideoSourceLoaderListener.swift +34 -0
- package/ios/VideoView.swift +224 -0
- package/package.json +59 -0
- package/plugin/build/tsconfig.tsbuildinfo +1 -0
- package/plugin/build/withExpoVideo.d.ts +7 -0
- package/plugin/build/withExpoVideo.js +38 -0
- package/src/NativeVideoModule.ts +20 -0
- package/src/NativeVideoModule.web.ts +1 -0
- package/src/NativeVideoView.ts +8 -0
- package/src/VideoModule.ts +59 -0
- package/src/VideoPlayer.tsx +67 -0
- package/src/VideoPlayer.types.ts +613 -0
- package/src/VideoPlayer.web.tsx +451 -0
- package/src/VideoPlayerEvents.types.ts +313 -0
- package/src/VideoThumbnail.ts +31 -0
- package/src/VideoView.tsx +86 -0
- package/src/VideoView.types.ts +165 -0
- package/src/VideoView.web.tsx +214 -0
- package/src/index.ts +46 -0
- package/src/resolveAssetSource.ts +2 -0
- package/src/resolveAssetSource.web.ts +17 -0
- 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
|
+
}
|