File Upload
The Visual Result
Behind the Scene
The following pieces have taken place:
- An audio file is
base64
-encoded in frontend - We send this
base64
-encoded string asformData
- We have nice package in backend to receive
formData
asinputStreams
- The input stream of audio file is processed by the following pipelines:
base64
-encoded touint8
m4a
file tomp3
file- The stream is piped into a
Duplex
stream (to adapt to api design offfmpeg
) - That
Duplex
stream is piped into azure'suploadStream()
method
Fontend: Upload an Audio Using FormData and Base64 Encoded String
import * as FileSystem from "expo-file-system"; const uploadFile = async (audioFileUri: string) => { const base64EncodedFile = await FileSystem.readAsStringAsync(audioFileUri, { encoding: FileSystem.EncodingType.Base64, }); dispatch( chatThunkAction.uploadVoice({ roomOid: selectedRoom._id, base64EncodedFile: { current: base64EncodedFile }, }) ) .unwrap() .finally(() => { FileSystem.deleteAsync(audioFileUri).catch(() => { msgUtil.error(`Cannot delete file: ${audioFileUri}`); }); }); };
Here the thunk action chatThunkAction.uploadVoice()
is defined as follows:
chatThunkAction = { uploadVoice: createAsyncThunk("chatSlice/upload-voice", async (props: { roomOid: string, base64EncodedFile: { current: string } }, api) => { const { base64EncodedFile, roomOid } = props; const formData = new FormData(); formData.append("file", base64EncodedFile.current); const res = await apiClient .post<WBResponse<undefined>>( apiRoutes.POST_UPLOAD_VOICE(roomOid), formData, { headers: { "Content-Type": "multipart/form-data" } } ); return processRes(res, api); }), ... }
We also pass an object to avoid copying the base64 encoded string (which is huge).
-
Now we have changed the file-upload procedures into a standard
form-data
approach that we have learnt from web developement. -
As a full-stack developer in nodejs we love to handle incoming file stream by multiparty!
Backend: Process the String Stream: Base64 to Uint8, From m4a To mp3, Pass Resulting Stream to azureClient.uploadStream()
We import a duplex to transform the base64-string-stream into a bytes-stream:
... // other dependencies import { Base64Decode } from "base64-stream"; import multiparty from 'multiparty'; const voiceUpload = async (req: Request, res: Response) => { const { roomOid } = req.query as { roomOid: string } const form = new multiparty.Form(); const msgDoc = await MessageModel.create({ roomOid, userOid: req.user?.userOid, type: "Voice" }); form.parse(req); form.on("part", async (inputStream) => { const uint8Stream = inputStream.pipe(new Base64Decode()); const bufferStream = new PassThrough(); const ffmpeg = ffmpegUtil.getFfmpeg(); ffmpeg(uint8Stream) .inputFormat("m4a") .audioCodec('libmp3lame') .audioChannels(1) .audioBitrate(128) .format('mp3') .pipe(bufferStream) const res_ = await client.getBlockBlobClient(filename).uploadStream(bufferStream); })
Summary for Backend
Since every step is merely processing stream, our data processing (from data conversion to file uploading to azure) is memory efficient as we never wait for the whole stream to complete before moving to the next step.
Apart from handling data conversion in stream, we also discussed zip stream in the past! Check this out!
File Download
Example of Redesigned Image Component for API That Returns a Stream
When <Image source={{ uri: imageUrl }}/>
fails, it is possible that the API returns a stream (chunks), then you may try the following:
export default (props: { imageUri: string }) => { const { imageUri } = props; const [base64, setBase64] = useState(""); const [id, setId] = useState(""); useEffect(() => { RNFetchBlob .fetch('GET', imageUri) .then((res) => { setBase64("data:image/jpeg;base64," + res.base64()); setId(uuid.v4() as string); }) .catch((err) => { msgUtil.error(err); }) }, []); if (!base64) { return null; } return ( <Image source={{ uri: base64 }} key={id} style={{ width: 180, height: 300, marginTop: 10, borderRadius: 4 }} /> ) }