Skip to content

Commit fb775c1

Browse files
authored
Add ability for media players to play base64 on iOS (#887)
1 parent 61453e8 commit fb775c1

File tree

6 files changed

+97
-40
lines changed

6 files changed

+97
-40
lines changed

example/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636
"react-native-safe-area-context": "4.8.2",
3737
"react-native-screens": "~3.29.0",
3838
"react-native-web": "~0.19.6",
39-
"uuid": "3.4.0"
39+
"uuid": "^9.0.1"
4040
},
4141
"devDependencies": {
4242
"@babel/core": "^7.20.0",

packages/core/package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,21 +60,22 @@
6060
"react-native-confirmation-code-field": "^7.3.1",
6161
"react-native-deck-swiper": "^2.0.12",
6262
"react-native-dropdown-picker": "^5.4.7-beta.1",
63-
"react-native-select-dropdown": "4.0.1",
6463
"react-native-gesture-handler": "~2.14.0",
6564
"react-native-keyboard-aware-scroll-view": "^0.9.5",
6665
"react-native-markdown-display": "^7.0.0-alpha.2",
6766
"react-native-modal-datetime-picker": "^13.0.0",
6867
"react-native-pager-view": "6.2.3",
6968
"react-native-reanimated": "~3.6.2",
69+
"react-native-select-dropdown": "4.0.1",
7070
"react-native-shadow-2": "^7.0.7",
7171
"react-native-svg": "14.1.0",
7272
"react-native-swipe-list-view": "^3.2.9",
7373
"react-native-tab-view": "^3.4.0",
7474
"react-native-typography": "^1.4.1",
7575
"react-native-web-swiper": "^2.2.3",
7676
"react-native-youtube-iframe": "^2.2.2",
77-
"react-youtube": "^10.1.0"
77+
"react-youtube": "^10.1.0",
78+
"uuid": "^9.0.1"
7879
},
7980
"peerDependencies": {
8081
"react-native-avoid-softinput": "^4.0.1"
@@ -92,6 +93,7 @@
9293
"@types/lodash.isnumber": "^3.0.6",
9394
"@types/lodash.omit": "^4.5.6",
9495
"@types/lodash.tonumber": "^4.0.6",
96+
"@types/uuid": "^9.0.8",
9597
"metro-react-native-babel-preset": "^0.77.0",
9698
"react-native-avoid-softinput": "^4.0.1"
9799
},

packages/core/src/components/MediaPlayer/AudioPlayer/HeadlessAudioPlayer.tsx

Lines changed: 8 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import {
66
InterruptionModeAndroid,
77
} from "expo-av";
88
import { HeadlessAudioPlayerProps } from "./AudioPlayerCommon";
9-
import { mapToMediaPlayerStatus } from "../MediaPlayerCommon";
9+
import {
10+
mapToMediaPlayerStatus,
11+
normalizeBase64Source,
12+
useSourceDeepCompareEffect,
13+
} from "../MediaPlayerCommon";
1014
import type { MediaPlayerRef } from "../MediaPlayerCommon";
1115
import MediaPlaybackWrapper from "../MediaPlaybackWrapper";
1216

@@ -89,7 +93,9 @@ const HeadlessAudioPlayer = React.forwardRef<
8993
isError: false,
9094
});
9195

92-
const { sound } = await Audio.Sound.createAsync(source);
96+
let finalSource = await normalizeBase64Source(source);
97+
98+
const { sound } = await Audio.Sound.createAsync(finalSource);
9399
setCurrentSound(sound);
94100
sound.setOnPlaybackStatusUpdate(onPlaybackStatusUpdate);
95101
};
@@ -111,33 +117,4 @@ const HeadlessAudioPlayer = React.forwardRef<
111117
}
112118
);
113119

114-
// The source provided into the AudioPlayer can be of type {uri: "some uri"}
115-
// In the case that this object is created inline, each rerender provides a new source object because a new object is initialized everytime
116-
// This creates an issue with being a useEffect dependency
117-
//
118-
// This creates variants of useEffect that checks deep equality of 'uri' to determine if dependency changed or not
119-
// Follows: https://stackoverflow.com/a/54096391
120-
function sourceDeepCompareEquals(a: any, b: any) {
121-
if (a?.uri && b?.uri) {
122-
return a.uri === b.uri;
123-
}
124-
return a === b;
125-
}
126-
127-
function useSourceDeepCompareMemoize(value: any) {
128-
const ref = React.useRef();
129-
if (!sourceDeepCompareEquals(value, ref.current)) {
130-
ref.current = value;
131-
}
132-
return ref.current;
133-
}
134-
135-
function useSourceDeepCompareEffect(
136-
callback: React.EffectCallback,
137-
dependencies: React.DependencyList
138-
) {
139-
// eslint-disable-next-line react-hooks/exhaustive-deps
140-
React.useEffect(callback, dependencies.map(useSourceDeepCompareMemoize));
141-
}
142-
143120
export default HeadlessAudioPlayer;

packages/core/src/components/MediaPlayer/MediaPlayerCommon.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { AVPlaybackSource, AVPlaybackStatus } from "expo-av";
2+
import * as FileSystem from "expo-file-system";
3+
import { v4 as uuid } from "uuid";
4+
import { Platform } from "react-native";
5+
import React from "react";
26

37
export interface MediaPlayerStatus {
48
isPlaying: boolean;
@@ -50,3 +54,55 @@ export function mapToMediaPlayerStatus(
5054
error: status.error,
5155
};
5256
}
57+
58+
// https://stackoverflow.com/a/7874175/8805150
59+
const BASE_64_REGEX =
60+
/^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/;
61+
62+
/**
63+
* Base64 strings are not playable on iOS and needs to be saved to a file before playing
64+
*/
65+
export async function normalizeBase64Source(
66+
source: AVPlaybackSource
67+
): Promise<AVPlaybackSource> {
68+
const uri: string | undefined = (source as any)?.uri;
69+
70+
if (Platform.OS === "ios" && uri && uri.match(BASE_64_REGEX)) {
71+
const fileName = `${FileSystem.cacheDirectory}${uuid()}`;
72+
await FileSystem.writeAsStringAsync(fileName, uri, {
73+
encoding: FileSystem.EncodingType.Base64,
74+
});
75+
return { uri: fileName };
76+
}
77+
78+
return source;
79+
}
80+
81+
// The source provided into the AudioPlayer can be of type {uri: "some uri"}
82+
// In the case that this object is created inline, each rerender provides a new source object because a new object is initialized everytime
83+
// This creates an issue with being a useEffect dependency
84+
//
85+
// This creates variants of useEffect that checks deep equality of 'uri' to determine if dependency changed or not
86+
// Follows: https://stackoverflow.com/a/54096391
87+
function sourceDeepCompareEquals(a: any, b: any) {
88+
if (a?.uri && b?.uri) {
89+
return a.uri === b.uri;
90+
}
91+
return a === b;
92+
}
93+
94+
function useSourceDeepCompareMemoize(value: any) {
95+
const ref = React.useRef();
96+
if (!sourceDeepCompareEquals(value, ref.current)) {
97+
ref.current = value;
98+
}
99+
return ref.current;
100+
}
101+
102+
export function useSourceDeepCompareEffect(
103+
callback: React.EffectCallback,
104+
dependencies: React.DependencyList
105+
) {
106+
// eslint-disable-next-line react-hooks/exhaustive-deps
107+
React.useEffect(callback, dependencies.map(useSourceDeepCompareMemoize));
108+
}

packages/core/src/components/MediaPlayer/VideoPlayer/VideoPlayer.tsx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,16 @@ import {
66
ResizeMode as ExpoResizeMode,
77
AVPlaybackStatus,
88
VideoFullscreenUpdate,
9+
AVPlaybackSource,
910
} from "expo-av";
1011
import { extractSizeStyles } from "../../../utilities";
1112
import MediaPlaybackWrapper from "../MediaPlaybackWrapper";
1213
import type { Playback } from "expo-av/src/AV";
13-
import { mapToMediaPlayerStatus } from "../MediaPlayerCommon";
14+
import {
15+
mapToMediaPlayerStatus,
16+
normalizeBase64Source,
17+
useSourceDeepCompareEffect,
18+
} from "../MediaPlayerCommon";
1419
import type { MediaPlayerRef, MediaPlayerProps } from "../MediaPlayerCommon";
1520

1621
type ResizeMode = "contain" | "cover" | "stretch";
@@ -36,6 +41,7 @@ const VideoPlayer = React.forwardRef<VideoPlayerRef, VideoPlayerProps>(
3641
posterResizeMode = "cover",
3742
onPlaybackStatusUpdate: onPlaybackStatusUpdateProp,
3843
onPlaybackFinish,
44+
source,
3945
...rest
4046
},
4147
ref
@@ -44,6 +50,8 @@ const VideoPlayer = React.forwardRef<VideoPlayerRef, VideoPlayerProps>(
4450
React.useState<VideoPlayerComponent | null>();
4551
const [isPlaying, setIsPlaying] = React.useState(false);
4652
const [isFullscreen, setIsFullscreen] = React.useState(false);
53+
const [currentSource, setCurrentSource] =
54+
React.useState<AVPlaybackSource>();
4755
const mediaPlaybackWrapperRef = React.useRef<MediaPlayerRef>(null);
4856

4957
const sizeStyles = extractSizeStyles(style);
@@ -110,6 +118,14 @@ const VideoPlayer = React.forwardRef<VideoPlayerRef, VideoPlayerProps>(
110118
[toggleFullscreen, isPlaying]
111119
);
112120

121+
useSourceDeepCompareEffect(() => {
122+
const updateSource = async () => {
123+
const finalSource = await normalizeBase64Source(source);
124+
setCurrentSource(finalSource);
125+
};
126+
updateSource();
127+
}, [source]);
128+
113129
return (
114130
<MediaPlaybackWrapper
115131
media={videoMediaObject as Playback | undefined}
@@ -125,6 +141,7 @@ const VideoPlayer = React.forwardRef<VideoPlayerRef, VideoPlayerProps>(
125141
posterStyle={[sizeStyles, { resizeMode: posterResizeMode }]}
126142
onPlaybackStatusUpdate={onPlaybackStatusUpdate}
127143
onFullscreenUpdate={(e) => onFullscreenUpdate(e.fullscreenUpdate)}
144+
source={currentSource}
128145
{...rest}
129146
/>
130147
</MediaPlaybackWrapper>

yarn.lock

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4267,6 +4267,11 @@
42674267
resolved "https://registry.yarnpkg.com/@types/tough-cookie/-/tough-cookie-4.0.2.tgz#6286b4c7228d58ab7866d19716f3696e03a09397"
42684268
integrity sha512-Q5vtl1W5ue16D+nIaW8JWebSSraJVlK+EthKn7e7UcD4KWsaSJ8BqGPXNaPghgtcn/fhvrN17Tv8ksUsQpiplw==
42694269

4270+
"@types/uuid@^9.0.8":
4271+
version "9.0.8"
4272+
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba"
4273+
integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==
4274+
42704275
"@types/ws@^8.5.1":
42714276
version "8.5.4"
42724277
resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.4.tgz#bb10e36116d6e570dd943735f86c933c1587b8a5"
@@ -15204,11 +15209,6 @@ [email protected]:
1520415209
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
1520515210
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
1520615211

15207-
15208-
version "3.4.0"
15209-
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
15210-
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
15211-
1521215212
uuid@^7.0.3:
1521315213
version "7.0.3"
1521415214
resolved "https://registry.yarnpkg.com/uuid/-/uuid-7.0.3.tgz#c5c9f2c8cf25dc0a372c4df1441c41f5bd0c680b"
@@ -15219,6 +15219,11 @@ uuid@^8.0.0, uuid@^8.3.2:
1521915219
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
1522015220
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
1522115221

15222+
uuid@^9.0.1:
15223+
version "9.0.1"
15224+
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.1.tgz#e188d4c8853cc722220392c424cd637f32293f30"
15225+
integrity sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==
15226+
1522215227
v8-compile-cache-lib@^3.0.1:
1522315228
version "3.0.1"
1522415229
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"

0 commit comments

Comments
 (0)