1 | import { Platform } from '@unimodules/core';
|
2 | import { Asset } from 'expo-asset';
|
3 |
|
4 | import ExponentAV from './ExponentAV';
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 | export enum PitchCorrectionQuality {
|
15 | Low = ExponentAV && ExponentAV.Qualities && ExponentAV.Qualities.Low,
|
16 | Medium = ExponentAV && ExponentAV.Qualities && ExponentAV.Qualities.Medium,
|
17 | High = ExponentAV && ExponentAV.Qualities && ExponentAV.Qualities.High,
|
18 | }
|
19 |
|
20 | export type AVPlaybackSource =
|
21 | | number
|
22 | | {
|
23 | uri: string;
|
24 | overrideFileExtensionAndroid?: string;
|
25 | headers?: { [fieldName: string]: string };
|
26 | }
|
27 | | Asset;
|
28 |
|
29 | export type AVPlaybackNativeSource = {
|
30 | uri: string;
|
31 | overridingExtension?: string | null;
|
32 | headers?: { [fieldName: string]: string };
|
33 | };
|
34 |
|
35 | export type AVPlaybackStatus =
|
36 | | {
|
37 | isLoaded: false;
|
38 | androidImplementation?: string;
|
39 | error?: string;
|
40 | }
|
41 | | {
|
42 | isLoaded: true;
|
43 | androidImplementation?: string;
|
44 |
|
45 | uri: string;
|
46 |
|
47 | progressUpdateIntervalMillis: number;
|
48 | durationMillis?: number;
|
49 | positionMillis: number;
|
50 | playableDurationMillis?: number;
|
51 | seekMillisToleranceBefore?: number;
|
52 | seekMillisToleranceAfter?: number;
|
53 |
|
54 | shouldPlay: boolean;
|
55 | isPlaying: boolean;
|
56 | isBuffering: boolean;
|
57 |
|
58 | rate: number;
|
59 | shouldCorrectPitch: boolean;
|
60 | volume: number;
|
61 | isMuted: boolean;
|
62 | isLooping: boolean;
|
63 |
|
64 | didJustFinish: boolean;
|
65 | };
|
66 |
|
67 | export type AVPlaybackStatusToSet = {
|
68 | androidImplementation?: string;
|
69 | progressUpdateIntervalMillis?: number;
|
70 | positionMillis?: number;
|
71 | seekMillisToleranceBefore?: number;
|
72 | seekMillisToleranceAfter?: number;
|
73 | shouldPlay?: boolean;
|
74 | rate?: number;
|
75 | shouldCorrectPitch?: boolean;
|
76 | volume?: number;
|
77 | isMuted?: boolean;
|
78 | isLooping?: boolean;
|
79 | pitchCorrectionQuality?: PitchCorrectionQuality;
|
80 | };
|
81 |
|
82 | export const _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS: number = 500;
|
83 | export const _DEFAULT_INITIAL_PLAYBACK_STATUS: AVPlaybackStatusToSet = {
|
84 | positionMillis: 0,
|
85 | progressUpdateIntervalMillis: _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS,
|
86 | shouldPlay: false,
|
87 | rate: 1.0,
|
88 | shouldCorrectPitch: false,
|
89 | volume: 1.0,
|
90 | isMuted: false,
|
91 | isLooping: false,
|
92 | };
|
93 |
|
94 | export function getNativeSourceFromSource(
|
95 | source?: AVPlaybackSource | null
|
96 | ): AVPlaybackNativeSource | null {
|
97 | let uri: string | null = null;
|
98 | let overridingExtension: string | null = null;
|
99 | let headers: { [fieldName: string]: string } | undefined;
|
100 |
|
101 | if (typeof source === 'string' && Platform.OS === 'web') {
|
102 | return {
|
103 | uri: source,
|
104 | overridingExtension,
|
105 | headers,
|
106 | };
|
107 | }
|
108 |
|
109 | const asset: Asset | null = _getAssetFromPlaybackSource(source);
|
110 | if (asset != null) {
|
111 | uri = asset.localUri || asset.uri;
|
112 | } else if (
|
113 | source != null &&
|
114 | typeof source !== 'number' &&
|
115 | 'uri' in source &&
|
116 | typeof source.uri === 'string'
|
117 | ) {
|
118 | uri = source.uri;
|
119 | }
|
120 |
|
121 | if (uri == null) {
|
122 | return null;
|
123 | }
|
124 |
|
125 | if (
|
126 | source != null &&
|
127 | typeof source !== 'number' &&
|
128 | 'overrideFileExtensionAndroid' in source &&
|
129 | typeof source.overrideFileExtensionAndroid === 'string'
|
130 | ) {
|
131 | overridingExtension = source.overrideFileExtensionAndroid;
|
132 | }
|
133 |
|
134 | if (
|
135 | source != null &&
|
136 | typeof source !== 'number' &&
|
137 | 'headers' in source &&
|
138 | typeof source.headers === 'object'
|
139 | ) {
|
140 | headers = source.headers;
|
141 | }
|
142 | return { uri, overridingExtension, headers };
|
143 | }
|
144 |
|
145 | function _getAssetFromPlaybackSource(source?: AVPlaybackSource | null): Asset | null {
|
146 | if (source == null) {
|
147 | return null;
|
148 | }
|
149 |
|
150 | let asset: Asset | null = null;
|
151 | if (typeof source === 'number') {
|
152 | asset = Asset.fromModule(source);
|
153 | } else if (source instanceof Asset) {
|
154 | asset = source;
|
155 | }
|
156 | return asset;
|
157 | }
|
158 |
|
159 | export function assertStatusValuesInBounds(status: AVPlaybackStatusToSet): void {
|
160 | if (typeof status.rate === 'number' && (status.rate < 0 || status.rate > 32)) {
|
161 | throw new RangeError('Rate value must be between 0.0 and 32.0');
|
162 | }
|
163 | if (typeof status.volume === 'number' && (status.volume < 0 || status.volume > 1)) {
|
164 | throw new RangeError('Volume value must be between 0.0 and 1.0');
|
165 | }
|
166 | }
|
167 |
|
168 | export async function getNativeSourceAndFullInitialStatusForLoadAsync(
|
169 | source: AVPlaybackSource | null,
|
170 | initialStatus: AVPlaybackStatusToSet | null,
|
171 | downloadFirst: boolean
|
172 | ): Promise<{
|
173 | nativeSource: AVPlaybackNativeSource;
|
174 | fullInitialStatus: AVPlaybackStatusToSet;
|
175 | }> {
|
176 |
|
177 | const fullInitialStatus: AVPlaybackStatusToSet =
|
178 | initialStatus == null
|
179 | ? _DEFAULT_INITIAL_PLAYBACK_STATUS
|
180 | : {
|
181 | ..._DEFAULT_INITIAL_PLAYBACK_STATUS,
|
182 | ...initialStatus,
|
183 | };
|
184 | assertStatusValuesInBounds(fullInitialStatus);
|
185 |
|
186 | if (typeof source === 'string' && Platform.OS === 'web') {
|
187 | return {
|
188 | nativeSource: {
|
189 | uri: source,
|
190 | overridingExtension: null,
|
191 | },
|
192 | fullInitialStatus,
|
193 | };
|
194 | }
|
195 |
|
196 |
|
197 | const asset = _getAssetFromPlaybackSource(source);
|
198 | if (downloadFirst && asset) {
|
199 |
|
200 | await asset.downloadAsync();
|
201 | }
|
202 |
|
203 |
|
204 | const nativeSource: AVPlaybackNativeSource | null = getNativeSourceFromSource(source);
|
205 |
|
206 | if (nativeSource === null) {
|
207 | throw new Error(`Cannot load an AV asset from a null playback source`);
|
208 | }
|
209 |
|
210 | return { nativeSource, fullInitialStatus };
|
211 | }
|
212 |
|
213 | export function getUnloadedStatus(error: string | null = null): AVPlaybackStatus {
|
214 | return {
|
215 | isLoaded: false,
|
216 | ...(error ? { error } : null),
|
217 | };
|
218 | }
|
219 |
|
220 | export interface AV {
|
221 | setStatusAsync(status: AVPlaybackStatusToSet): Promise<AVPlaybackStatus>;
|
222 | getStatusAsync(): Promise<AVPlaybackStatus>;
|
223 | }
|
224 |
|
225 | export interface Playback extends AV {
|
226 | playAsync(): Promise<AVPlaybackStatus>;
|
227 | loadAsync(
|
228 | source: AVPlaybackSource,
|
229 | initialStatus: AVPlaybackStatusToSet,
|
230 | downloadAsync: boolean
|
231 | ): Promise<AVPlaybackStatus>;
|
232 | unloadAsync(): Promise<AVPlaybackStatus>;
|
233 | playFromPositionAsync(
|
234 | positionMillis: number,
|
235 | tolerances?: { toleranceMillisBefore?: number; toleranceMillisAfter?: number }
|
236 | ): Promise<AVPlaybackStatus>;
|
237 | pauseAsync(): Promise<AVPlaybackStatus>;
|
238 | stopAsync(): Promise<AVPlaybackStatus>;
|
239 | replayAsync(status: AVPlaybackStatusToSet): Promise<AVPlaybackStatus>;
|
240 | setPositionAsync(
|
241 | positionMillis: number,
|
242 | tolerances?: { toleranceMillisBefore?: number; toleranceMillisAfter?: number }
|
243 | ): Promise<AVPlaybackStatus>;
|
244 | setRateAsync(
|
245 | rate: number,
|
246 | shouldCorrectPitch: boolean,
|
247 | pitchCorrectionQuality?: PitchCorrectionQuality
|
248 | ): Promise<AVPlaybackStatus>;
|
249 | setVolumeAsync(volume: number): Promise<AVPlaybackStatus>;
|
250 | setIsMutedAsync(isMuted: boolean): Promise<AVPlaybackStatus>;
|
251 | setIsLoopingAsync(isLooping: boolean): Promise<AVPlaybackStatus>;
|
252 | setProgressUpdateIntervalAsync(progressUpdateIntervalMillis: number): Promise<AVPlaybackStatus>;
|
253 | }
|
254 |
|
255 |
|
256 |
|
257 |
|
258 |
|
259 | export const PlaybackMixin = {
|
260 | async playAsync(): Promise<AVPlaybackStatus> {
|
261 | return ((this as any) as Playback).setStatusAsync({ shouldPlay: true });
|
262 | },
|
263 |
|
264 | async playFromPositionAsync(
|
265 | positionMillis: number,
|
266 | tolerances: { toleranceMillisBefore?: number; toleranceMillisAfter?: number } = {}
|
267 | ): Promise<AVPlaybackStatus> {
|
268 | return ((this as any) as Playback).setStatusAsync({
|
269 | positionMillis,
|
270 | shouldPlay: true,
|
271 | seekMillisToleranceAfter: tolerances.toleranceMillisAfter,
|
272 | seekMillisToleranceBefore: tolerances.toleranceMillisBefore,
|
273 | });
|
274 | },
|
275 |
|
276 | async pauseAsync(): Promise<AVPlaybackStatus> {
|
277 | return ((this as any) as Playback).setStatusAsync({ shouldPlay: false });
|
278 | },
|
279 |
|
280 | async stopAsync(): Promise<AVPlaybackStatus> {
|
281 | return ((this as any) as Playback).setStatusAsync({ positionMillis: 0, shouldPlay: false });
|
282 | },
|
283 |
|
284 | async setPositionAsync(
|
285 | positionMillis: number,
|
286 | tolerances: { toleranceMillisBefore?: number; toleranceMillisAfter?: number } = {}
|
287 | ): Promise<AVPlaybackStatus> {
|
288 | return ((this as any) as Playback).setStatusAsync({
|
289 | positionMillis,
|
290 | seekMillisToleranceAfter: tolerances.toleranceMillisAfter,
|
291 | seekMillisToleranceBefore: tolerances.toleranceMillisBefore,
|
292 | });
|
293 | },
|
294 |
|
295 | async setRateAsync(
|
296 | rate: number,
|
297 | shouldCorrectPitch: boolean = false,
|
298 | pitchCorrectionQuality: PitchCorrectionQuality = PitchCorrectionQuality.Low
|
299 | ): Promise<AVPlaybackStatus> {
|
300 | return ((this as any) as Playback).setStatusAsync({
|
301 | rate,
|
302 | shouldCorrectPitch,
|
303 | pitchCorrectionQuality,
|
304 | });
|
305 | },
|
306 |
|
307 | async setVolumeAsync(volume: number): Promise<AVPlaybackStatus> {
|
308 | return ((this as any) as Playback).setStatusAsync({ volume });
|
309 | },
|
310 |
|
311 | async setIsMutedAsync(isMuted: boolean): Promise<AVPlaybackStatus> {
|
312 | return ((this as any) as Playback).setStatusAsync({ isMuted });
|
313 | },
|
314 |
|
315 | async setIsLoopingAsync(isLooping: boolean): Promise<AVPlaybackStatus> {
|
316 | return ((this as any) as Playback).setStatusAsync({ isLooping });
|
317 | },
|
318 |
|
319 | async setProgressUpdateIntervalAsync(
|
320 | progressUpdateIntervalMillis: number
|
321 | ): Promise<AVPlaybackStatus> {
|
322 | return ((this as any) as Playback).setStatusAsync({ progressUpdateIntervalMillis });
|
323 | },
|
324 | };
|