Skip to content

Commit cae81d0

Browse files
authored
VideoPlayer component + refactor AudioPlayer (#697)
* Added video player + refactored audio player to share some logic with video player * Added clarifying comment to video player * Added VideoPlayer tests * Updated testing config to allow expo in tests * Added audio player tests
1 parent 532e773 commit cae81d0

File tree

15 files changed

+699
-122
lines changed

15 files changed

+699
-122
lines changed

example/src/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ import SwipeableItemExample from "./SwipeableItemExample";
6262
import SectionListExample from "./SectionListExample";
6363
import LinearProgressExample from "./LinearProgressExample";
6464
import CircularProgressExample from "./CircularProgressExample";
65+
import VideoPlayerExample from "./VideoPlayerExample";
6566

6667
const ROUTES = {
6768
AudioPlayer: AudioPlayerExample,
@@ -99,6 +100,7 @@ const ROUTES = {
99100
SectionList: SectionListExample,
100101
LinearProgress: LinearProgressExample,
101102
CircularProgress: CircularProgressExample,
103+
VideoPlayer: VideoPlayerExample,
102104
};
103105

104106
let customFonts = {

example/src/VideoPlayerExample.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import React from "react";
2+
import Section, { Container } from "./Section";
3+
import { Text } from "react-native";
4+
import { VideoPlayer, VideoPlayerRef, Button } from "@draftbit/ui";
5+
6+
const VideoPlayerExample: React.FC = () => {
7+
const videoPlayerRef = React.useRef<VideoPlayerRef>(null);
8+
const [playeState, setPlayerState] = React.useState<object>();
9+
10+
return (
11+
<Container style={{}}>
12+
<Section style={{}} title="VideoPlayer (Default with native controls)">
13+
<VideoPlayer
14+
style={{ width: 350, height: 250 }}
15+
source={{
16+
uri: "http://static.draftbit.com/videos/intro-to-draftbit.mp4",
17+
}}
18+
useNativeControls
19+
resizeMode="cover"
20+
/>
21+
</Section>
22+
<Section style={{}} title="VideoPlayer (Poster and custom controls)">
23+
<VideoPlayer
24+
ref={videoPlayerRef}
25+
style={{ width: 350, height: 250 }}
26+
source={{
27+
uri: "https://d23dyxeqlo5psv.cloudfront.net/big_buck_bunny.mp4",
28+
}}
29+
useNativeControls={false}
30+
posterSource={{
31+
uri: "https://fujifilm-x.com/wp-content/uploads/2021/01/gfx100s_sample_04_thum-1.jpg",
32+
}}
33+
usePoster
34+
onPlaybackStatusUpdate={(status) => setPlayerState(status)}
35+
onPlaybackFinish={() => console.log("Finished playback")}
36+
/>
37+
<Button
38+
//@ts-ignore
39+
title="Toggle playback"
40+
onPress={() => {
41+
videoPlayerRef?.current?.togglePlayback();
42+
}}
43+
/>
44+
<Button
45+
//@ts-ignore
46+
title="Seek player to 10 second mark"
47+
onPress={() => {
48+
videoPlayerRef?.current?.seekToPosition(10000);
49+
}}
50+
/>
51+
<Button
52+
//@ts-ignore
53+
title="Toggle player full screen"
54+
onPress={() => {
55+
videoPlayerRef?.current?.toggleFullscreen();
56+
}}
57+
/>
58+
<Text>{`Current player state: ${JSON.stringify(playeState)}`}</Text>
59+
</Section>
60+
</Container>
61+
);
62+
};
63+
64+
export default VideoPlayerExample;
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
import React from "react";
2+
import { act, render, screen, fireEvent } from "@testing-library/react-native";
3+
import {
4+
default as AudioPlayer,
5+
AudioPlayerRef,
6+
} from "../../components/MediaPlayer/AudioPlayer";
7+
8+
const mockAudioSource = {
9+
uri: "audio-uri",
10+
};
11+
12+
const mockPlayAsync = jest.fn();
13+
const mockPauseAsync = jest.fn();
14+
const mockUnloadAsync = jest.fn();
15+
16+
// To ignore the warning: 'When testing, code that causes React state updates should be wrapped into act'
17+
// This is caused because state is updated in the useEffect, this is intentional and not an issue, so we can ignore it
18+
console.error = jest.fn();
19+
20+
jest.mock("expo-av", () => {
21+
const original = jest.requireActual("expo-av");
22+
23+
class Audio {
24+
static setAudioModeAsync = () => {};
25+
static Sound = {
26+
onPlaybackStatusUpdate: (_) => {},
27+
createAsync: function () {
28+
return {
29+
sound: {
30+
setOnPlaybackStatusUpdate: (callback) =>
31+
(this.onPlaybackStatusUpdate = callback),
32+
playAsync: () => {
33+
this.onPlaybackStatusUpdate({
34+
isLoaded: true,
35+
isPlaying: true,
36+
});
37+
mockPlayAsync();
38+
},
39+
pauseAsync: mockPauseAsync,
40+
unloadAsync: mockUnloadAsync,
41+
setPositionAsync: (position: number) => {
42+
this.onPlaybackStatusUpdate({
43+
isLoaded: true,
44+
positionMillis: position,
45+
});
46+
},
47+
},
48+
};
49+
},
50+
};
51+
}
52+
53+
return { ...original, Audio };
54+
});
55+
56+
beforeEach(() => {
57+
jest.clearAllMocks();
58+
});
59+
60+
describe("AudioPlayer tests", () => {
61+
test("should render an interface when in 'interface' mode", () => {
62+
render(<AudioPlayer mode="interface" source={mockAudioSource} />);
63+
64+
const playerInterface = screen.queryByTestId("audio-player-interface");
65+
expect(playerInterface).toBeTruthy();
66+
});
67+
68+
test("should not render an interface when in 'headless' mode", () => {
69+
render(<AudioPlayer mode="headless" source={mockAudioSource} />);
70+
71+
const playerInterface = screen.queryByTestId("audio-player-interface");
72+
() => expect(playerInterface).toBeFalsy();
73+
});
74+
75+
test("should render playback icon when hidePlaybackIcon is false", () => {
76+
render(<AudioPlayer source={mockAudioSource} hidePlaybackIcon={false} />);
77+
78+
const playbackIcon = screen.queryByTestId("audio-player-playback-icon");
79+
() => expect(playbackIcon).toBeTruthy();
80+
});
81+
82+
test("should not render playback icon when hidePlaybackIcon is true", () => {
83+
render(<AudioPlayer source={mockAudioSource} hidePlaybackIcon={true} />);
84+
85+
const playbackIcon = screen.queryByTestId("audio-player-playback-icon");
86+
() => expect(playbackIcon).toBeFalsy();
87+
});
88+
89+
test("should render duration when hideDuration is false", () => {
90+
render(<AudioPlayer source={mockAudioSource} hideDuration={false} />);
91+
92+
const duration = screen.queryByTestId("audio-player-duration");
93+
() => expect(duration).toBeTruthy();
94+
});
95+
96+
test("should not render duration when hideDuration is true", () => {
97+
render(<AudioPlayer source={mockAudioSource} hideDuration={true} />);
98+
99+
const duration = screen.queryByTestId("audio-player-duration");
100+
() => expect(duration).toBeFalsy();
101+
});
102+
103+
test("should render slider when hideSlider is false", () => {
104+
render(<AudioPlayer source={mockAudioSource} hideSlider={false} />);
105+
106+
const slider = screen.queryByTestId("audio-player-slider");
107+
() => expect(slider).toBeTruthy();
108+
});
109+
110+
test("should not render slider when hideSlider is true", () => {
111+
render(<AudioPlayer source={mockAudioSource} hideSlider={true} />);
112+
113+
const slider = screen.queryByTestId("audio-player-slider");
114+
() => expect(slider).toBeFalsy();
115+
});
116+
117+
test("should play and pause audio when clicking playback icon", async () => {
118+
const ref = React.createRef<AudioPlayerRef>();
119+
120+
render(<AudioPlayer ref={ref} source={mockAudioSource} />);
121+
122+
await waitForSoundToLoad();
123+
124+
const playbackIcon = await screen.findByTestId(
125+
"audio-player-playback-icon"
126+
);
127+
128+
act(() => {
129+
fireEvent.press(playbackIcon);
130+
});
131+
expect(mockPlayAsync).toBeCalled();
132+
133+
act(() => {
134+
fireEvent.press(playbackIcon);
135+
});
136+
expect(mockPauseAsync).toBeCalled();
137+
});
138+
139+
test("should togglePlayback play and pause audio", async () => {
140+
const ref = React.createRef<AudioPlayerRef>();
141+
142+
render(<AudioPlayer ref={ref} source={mockAudioSource} />);
143+
144+
await act(async () => {
145+
await waitForSoundToLoad();
146+
ref.current?.togglePlayback();
147+
});
148+
expect(mockPlayAsync).toBeCalled();
149+
150+
act(() => {
151+
ref.current?.togglePlayback();
152+
});
153+
expect(mockPauseAsync).toBeCalled();
154+
});
155+
156+
test("should audio be cleaned up/unloaded when unmounting", async () => {
157+
render(<AudioPlayer source={mockAudioSource} />);
158+
159+
await waitForSoundToLoad();
160+
screen.unmount();
161+
expect(mockUnloadAsync).toBeCalled();
162+
});
163+
164+
test("should seekToPosition change audio position", async () => {
165+
const ref = React.createRef<AudioPlayerRef>();
166+
const position = 30000;
167+
const onPlaybackStatusUpdate = jest.fn();
168+
render(
169+
<AudioPlayer
170+
ref={ref}
171+
source={mockAudioSource}
172+
onPlaybackStatusUpdate={onPlaybackStatusUpdate}
173+
/>
174+
);
175+
176+
await act(async () => {
177+
await waitForSoundToLoad();
178+
ref.current?.seekToPosition(position);
179+
});
180+
expect(onPlaybackStatusUpdate).toBeCalledWith(
181+
expect.objectContaining({ currentPositionMillis: position })
182+
);
183+
});
184+
});
185+
186+
async function waitForSoundToLoad() {
187+
/**
188+
* The sound/media object/reference is not instantly loaded in the Audio Player
189+
* This delay is enough to make sure it is loaded
190+
*
191+
* This is the simplest way to do it since mocks are being used and nothing is actually being 'loaded', just prevents it from being instant
192+
*/
193+
await new Promise((r) => setTimeout(r, 500));
194+
}

0 commit comments

Comments
 (0)