Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 32 additions & 15 deletions app/video-player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@ import { AppState, type AppStateStatus, StyleSheet, View } from "react-native";
const fetchStreamingPlaylistUrl = async (streamingUrl: string, accessToken: string): Promise<string> =>
(await requestAPI<{ playlist_url: string }>(streamingUrl, { accessToken })).playlist_url;

const isReleasedPlayerError = (error: unknown): boolean => {
const { code, message } = (error ?? {}) as { code?: string; message?: string };
if (code === "ERR_USING_RELEASED_SHARED_OBJECT" || code === "ERR_NATIVE_SHARED_OBJECT_NOT_FOUND") return true;
return /shared object that was already released|find the native shared object/i.test(message ?? "");
};

const withReleasedPlayerGuard = (operation: () => void) => {
try {
operation();
} catch (error) {
if (isReleasedPlayerError(error)) return;
throw error;
}
};

export default function VideoPlayerScreen() {
const { accessToken } = useAuth();
const { uri, streamingUrl, title, urlRedirectId, productFileId, purchaseId, initialPosition } = useLocalSearchParams<{
Expand Down Expand Up @@ -71,27 +86,29 @@ export default function VideoPlayerScreen() {

useEffect(() => {
const subscription = AppState.addEventListener("change", (nextState: AppStateStatus) => {
if (nextState === "background" || nextState === "inactive") {
wasPlayingBeforeBackgroundRef.current = player.playing;
positionBeforeBackgroundRef.current = player.currentTime;
player.pause();
} else if (nextState === "active") {
const savedPosition = positionBeforeBackgroundRef.current;
if (savedPosition !== null && player.currentTime < savedPosition - 1) {
player.currentTime = savedPosition;
}
positionBeforeBackgroundRef.current = null;
if (wasPlayingBeforeBackgroundRef.current) {
player.play();
wasPlayingBeforeBackgroundRef.current = false;
withReleasedPlayerGuard(() => {
if (nextState === "background" || nextState === "inactive") {
wasPlayingBeforeBackgroundRef.current = player.playing;
positionBeforeBackgroundRef.current = player.currentTime;
player.pause();
} else if (nextState === "active") {
const savedPosition = positionBeforeBackgroundRef.current;
if (savedPosition !== null && player.currentTime < savedPosition - 1) {
player.currentTime = savedPosition;
}
positionBeforeBackgroundRef.current = null;
if (wasPlayingBeforeBackgroundRef.current) {
player.play();
wasPlayingBeforeBackgroundRef.current = false;
}
}
}
});
});

return () => subscription.remove();
}, [player]);

useEffect(() => () => player.pause(), [player]);
useEffect(() => () => withReleasedPlayerGuard(() => player.pause()), [player]);

useEffect(() => {
const subscription = player.addListener(
Expand Down
50 changes: 50 additions & 0 deletions tests/app/video-player.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,56 @@ describe("VideoPlayerScreen", () => {
expect(mockPlayer.pause).toHaveBeenCalled();
});

it("does not crash on unmount when the player has already been released", () => {
const { unmount } = renderScreen();
mockPlayer.pause.mockImplementation(() => {
throw Object.assign(new Error("Cannot use shared object that was already released"), {
code: "ERR_USING_RELEASED_SHARED_OBJECT",
});
});

expect(() => unmount()).not.toThrow();
});

it("does not crash on background when the player has already been released", () => {
renderScreen();
mockPlayer.pause.mockImplementation(() => {
throw Object.assign(new Error("Cannot use shared object that was already released"), {
code: "ERR_USING_RELEASED_SHARED_OBJECT",
});
});

expect(() => act(() => appStateCallback!("background"))).not.toThrow();
});

it("does not crash on unmount when the player call fails with the iOS not-found exception", () => {
const { unmount } = renderScreen();
mockPlayer.pause.mockImplementation(() => {
throw Object.assign(
new Error(
"Calling the 'pause' function has failed\n→ Caused by: Unable to find the native shared object associated with given JavaScript object",
),
{ code: "ERR_FUNCTION_CALL" },
);
});

expect(() => unmount()).not.toThrow();
});

it("does not crash on background when the player call fails with the iOS not-found exception", () => {
renderScreen();
mockPlayer.pause.mockImplementation(() => {
throw Object.assign(
new Error(
"Calling the 'pause' function has failed\n→ Caused by: Unable to find the native shared object associated with given JavaScript object",
),
{ code: "ERR_FUNCTION_CALL" },
);
});

expect(() => act(() => appStateCallback!("background"))).not.toThrow();
});

it("restores the playback position when returning from background", () => {
renderScreen();
mockPlayer.currentTime = 120;
Expand Down
Loading