UNPKG

9.61 kBPlain TextView Raw
1import { Platform } from '@unimodules/core';
2import { Asset } from 'expo-asset';
3
4import ExponentAV from './ExponentAV';
5// TODO add:
6// disableFocusOnAndroid
7// audio routes (at least did become noisy on android)
8// pan
9// pitch
10// API to explicitly request audio focus / session
11// API to select stream type on Android
12// subtitles API
13
14export 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
20export type AVPlaybackSource =
21 | number
22 | {
23 uri: string;
24 overrideFileExtensionAndroid?: string;
25 headers?: { [fieldName: string]: string };
26 }
27 | Asset;
28
29export type AVPlaybackNativeSource = {
30 uri: string;
31 overridingExtension?: string | null;
32 headers?: { [fieldName: string]: string };
33};
34
35export type AVPlaybackStatus =
36 | {
37 isLoaded: false;
38 androidImplementation?: string;
39 error?: string; // populated exactly once when an error forces the object to unload
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; // true exactly once when the track plays to finish
65 };
66
67export 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
82export const _DEFAULT_PROGRESS_UPDATE_INTERVAL_MILLIS: number = 500;
83export 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
94export 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
145function _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
159export 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
168export async function getNativeSourceAndFullInitialStatusForLoadAsync(
169 source: AVPlaybackSource | null,
170 initialStatus: AVPlaybackStatusToSet | null,
171 downloadFirst: boolean
172): Promise<{
173 nativeSource: AVPlaybackNativeSource;
174 fullInitialStatus: AVPlaybackStatusToSet;
175}> {
176 // Get the full initial status
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 // Download first if necessary.
197 const asset = _getAssetFromPlaybackSource(source);
198 if (downloadFirst && asset) {
199 // TODO we can download remote uri too once @nikki93 has integrated this into Asset
200 await asset.downloadAsync();
201 }
202
203 // Get the native source
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
213export function getUnloadedStatus(error: string | null = null): AVPlaybackStatus {
214 return {
215 isLoaded: false,
216 ...(error ? { error } : null),
217 };
218}
219
220export interface AV {
221 setStatusAsync(status: AVPlaybackStatusToSet): Promise<AVPlaybackStatus>;
222 getStatusAsync(): Promise<AVPlaybackStatus>;
223}
224
225export 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 * A mixin that defines common playback methods for A/V classes so they implement the `Playback`
257 * interface
258 */
259export 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};