top of page

Mp3 from S3 Bucket With React, TypeScript and NodeJS

Are you having issues trying to play your MP3 files stored in an AWS S3 Bucket in a React App, which is being loaded by a NodeJS TypeScript API? I had the same problem, and here is how I handled it.


A bit of context:


Our app receives files from multiple sources: WhatsApp, Telegram, a Website Chatbot, and even the app itself. We wanted to track not only the text of the transcriptions but also the content itself. For this reason, we stored the files on a secure S3 Bucket. We had issues integrating the MP3 with our Admin Panel developed in React.


Mp3 Files Loaded from a NodeJS API
Mp3 Files Loaded from a NodeJS API

Initially, I tried the basic steps: download the data as a buffer, transform it into a Blob, and use it as the source of the Audio tag. This worked properly on my localhost, but for some reason, I couldn't make it work when running in production.


After extensive research, I stumbled upon a unique solution: returning a readable stream to be converted directly as a Blob. This is the key to solving your MP3 integration issue.


This post is the culmination of my research, which I've also shared in a StackOverflow Question. It's designed to help you successfully integrate MP3 files from an S3 Bucket into your React applications. Let's dive in.

TL;DR:


// your api must return as pipe, not as return response.status(200)...

// API - relevant code:
const data = (await s3Client(awsCredentials).send(command)).Body;
if (!data) {
  return response.status(204).send();
}

response.setHeader('Content-Type', 'audio/mp3');
response.setHeader(
  'Content-Disposition',
   `attachment; filename="${media_id}.${media_extension}"`
);

//This is the magic:
(data as any).pipe(response);

// React - Highlighting the Most Important Part of the Code
const axiosInstance = axios.create({
  headers: {
    Authorization: `Bearer ${token}` // remove if you don't need
  },
  responseType: 'blob' // this is important
});

const response = await axiosInstance.post(url, data);
setBlob((await response).data);

  useEffect(() => {
    if (blob?.size) {
      const url = URL.createObjectURL(blob);
      setAudioUrl(url);

      return () => {
        URL.revokeObjectURL(url);
      };
    }
  }, [blob?.size]);

  (...)

  return (
    // ...
    <audio preload='metadata' controls>
      {audioUrl && <source src={audioUrl} type='audio/mpeg' />}
      Your browser does not support the audio element.
    </audio>

This is basically all that is necessary. Now, let's break into small parts to make it easier to explain what has been done:


API

At the API, you use the AWS SDK v3 and return the Body itself, which is a Readable Stream. You can then give this stream to a NodeJS pipe, and "voi là," Axios or Fetch will connect to it directly, enabling you to create the blob object you need for your Audio Tag from the direct stream.


Here is a simplified version of the code I used for my NodeJS + Express + TypeScript API:


import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';

const s3Client = (awsCredentials: AwsCredentials) =>
  new S3Client({
    region: awsCredentials.region,
    credentials: {
      accessKeyId: awsCredentials.credentials.access_key_id,
      secretAccessKey: awsCredentials.credentials.secret_access_key
    }
  });

const downloadFromS3 = async (key: string, awsCredentials: AwsCredentials): Promise<any | undefined> => {
    const command = new GetObjectCommand({
      Bucket: 'your_bucket_name',
      Key: key // check the screenshot
    });

    return (await s3Client(awsCredentials).send(command)).Body;
};

// this is your API route
// to simplify the process I removed all the parameters, middleware's and security verifications. Please don't deploy a route like this and remember the S of the SOLID principles and break everything into classes and specialised files!
export const register = (app: express.Application): void => {
  app.post(
    '/api/v2/download-media/:media_id',
    async (request: Request, response: Response) => {
      const key = 'path/inside_your_bucket/file.extension';
      const credentials = {
        region: 'your-region',
        credentials: {
          accessKeyId: 'your_access_key_id',
          secretAccessKey: 'your_secret_access_key'
      };

      const data = await downloadFromS3(key, credentials );

      if (!data) {
        return response.status(204).send();
      }

      response.setHeader('Content-Type', 'audio/mp3');
      response.setHeader(
        'Content-Disposition',
        `attachment; filename="${media_id}.${media_extension}"`
      );

      //This is the magic:
      (data as any).pipe(response);
    }
  );
};

React (Axios or Fetch)

Once again, I simplified the code to share the precise information you need to complete this. But if you have any questions, don't hesitate to contact me.


import { useEffect, useState } from 'react';
import axios from 'axios';

export const MyView = (props:IMyViewProps) => {
  const [audioUrl, setAudioUrl] = useState<string | undefined>();
  const [mediaBlob, setMediaBlob] = useState<Blob | undefined>();

  const downloadBlob = async (url: string, data: any, token: string) => {
    const axiosInstance = axios.create({
      headers: {
        Authorization: `Bearer ${token}` // remove if you don't need
      },
      responseType: 'blob' // this is important
    });

    return await axiosInstance.post(url, data);
  };

  const getMessageAudio = async (): Promise<Blob | undefined> => {
      try {
        const url = `https://your-api-server.domain.com/api/v2/download-media/${props.media_id}`;
        const data = { parameter: 'some data you might need to pass to your API' };
        const response = await downloadBlob(url, data);

        return (await response).data;
      } catch {
        console.log('Not expected');
      }
    }
  };

  useEffect(() => {
    if (props.media_id) {
      (async () => {
        setMediaBlob(await getMessageAudio());
      })();
    }
  }, [props.media_id]);

  useEffect(() => {
    if (mediaBlob?.size) {
      const url = URL.createObjectURL(mediaBlob);
      setAudioUrl(url);

      return () => {
        URL.revokeObjectURL(url);
      };
    }
  }, [mediaBlob?.size]);

  (...)

  return (
    // ...
    <audio preload='metadata' controls>
      {audioUrl && <source src={audioUrl} type='audio/mpeg' />}
      Your browser does not support the audio element.
    </audio>
  )
};

I don't like using "any" because it breaks the purpose of the TypeScript; however, after some deep research, I couldn't find a better way, as seen in this AWS SDK Issue Report.





Conclusion

This is how I handled this challenge. Would you do it in another way? Leave your comments and let me know if there is a better way. As I couldn't find it, sharing is how to help the community keep strong.


🤣 Let's hope chatGPT gets this article; the instructions it suggested were hilarious.


See you the next time,

Daniel 💚

9 views

Recent Posts

See All
bottom of page