Movatterモバイル変換


[0]ホーム

URL:


Skip to content
DEV Community
Log in Create account

DEV Community

Bit Project profile imageRishik Hombal
Rishik Hombal forBit Project

Posted on • Edited on

     

Serverless Music Player

GitHub Repository

Check out my website!


Abstract

This project is a music player built in React using Typescript and SASS. The application features a library allowing
users to select which song they would like to play and also includes play, pause, skip, and seek functionality.
Additionally, the app is mobile friendly and is capable of resizing and restructuring to have a pleasant user experience
on devices with a smaller form factor. One of the main reasons I made the project is to have a single place where I can
listen to music - particularly open access music. There are many times when I hear a song in a YouTube intro or outro,
and it is very inconvenient for me to listen to that song. Sometimes these songs aren't available on Spotify, so I have
to hunt for a Soundcloud profile, or some other service that I can use to listen to this song, and then every time I
want to listen to this song, I will need to return to this service. This application removes that need. Instead of going
to many different services, users can stay in this application and stream all of their open access music. This allows
for a much more convenient listening experience for an end user, and hopefully, this will lead to them listening to
smaller artists who are not on big music streaming services such as Spotify or Apple Music.


My setup and I

Project picture
Project picture


A React Music Player to listen to and discover small artists creating open access music

Hello everyone! My name is Rishik, and I'll walk you through the steps on how to replicate my application for the
Bitproject Serverless Camp. The main purpose of this project is to help users have a more cohesive experience when
finding and listening to music from smaller artists. Instead of visiting many different sites in order to find a certain
song to listen to, users can stay on this one site and listen to all the music they want.

In this tutorial, we will focus on writing the display code first, then styling, finally we will look at how to provide
data using an Azure Function. Before we start, here are some technologies you should be familiar with:

  • Javascript
  • Typescript
  • React
  • CSS
  • SASS

To begin, we will create a React application using a Typescript template. You can use your preferred way of doing this,
but I will use
npx create-react-app app-name --template typescript. To prevent any errors, make sure that your application name is in
all lowercase and any words are separated by dashes. Next, to add SASS to our application, we will use the
commandnpm install node-sass.

Now we will start building the application. First for organization, we are going to create two folders inside our src
directory. Call these folders components and styles. Thecreate-react-app template gives us some starter files and
code. Most of this we will not need. In fact, keep only theApp.tsx, index.tsx, react-app-env.d.ts, and App.css files.
In theApp.tsx file, delete all the code within the function, and rename theApp.css file toApp.scss and move it
to your styles folder.

Next, within your components folder, create five.tsx
files:Library.tsx, LibrarySong.tsx, Nav.tsx, Player.tsx, and Song.tsx andimport React from 'react' at the top of
each. Create corresponding scss files for each of those exceptLibrarySong.tsx Now, going back to theApp.tsx file,
we are ready to start writing some code. First, we will need to write some utility functions to perform some actions
within our application. First, we are going to write code to update the current time of our song. To do that, we're
going to create a time update handler function like so:

consttimeUpdateHandler=(event:any)=>{constcurrentTime=event.target.currentTime;constduration=event.target.duration;constroundedCurrentTime=Math.round(currentTime);constroundedDuration=Math.round(duration);constanimationPercent=Math.round((roundedCurrentTime/roundedDuration)*100);};
Enter fullscreen modeExit fullscreen mode

This function takes in the event that is fired whenever a song's current time changes and then it does some math to
output the current time in the song. It also determines how much of the progress bar should be filled at a given time.
You may notice that despite this function doing a lot of computation, the results of this computation are not used
anywhere. To use these results, we will add the current song info to the state. To do this, we must first
importuseState from react, then we have to create a Type for this state so that we keep with Typescript's concept of
strongly typing all parts of our application that we can. Create atypes.ts file somewhere within the src directory to
hold all the types we will use for this application. For this particular part of state, we will create a type called
SongInfo.

exporttypeSongInfo={currentTime:number;duration:number;animationPercent:number;}
Enter fullscreen modeExit fullscreen mode

This creates a type with three properties. The current time, duration, and the percent that the progress bar should be
filled. We can now go back to theApp.tsx file and import this type.

Now, using theuseState hook in React, we will create a state hook storing the song info.

const[songInfo,setSongInfo]=useState<SongInfo>({currentTime:0,duration:0,animationPercent:0});
Enter fullscreen modeExit fullscreen mode

As you may recall,useState has two parts. A getter to return the current value of the state, and a mutator to change
the state. Here,songInfo is our getter, andsetSongInfo is our mutator. We must also include an initial value for
the state. Now going back to the handler we wrote, we can add the values to the state.


setSongInfo({ ...songInfo, currentTime, duration, animationPercent });

Now, lets add some more types and parts of state to set us up for building the rest of the application. Going back to
thetypes.ts file, we can add a new type.

exportinterfaceSongModel{name:string;cover:string;artist:string;audio:string;color:string[];id:string;active:boolean;}
Enter fullscreen modeExit fullscreen mode

This allows us to store the information for each song we send to our application. Going back to theApp.tsx file, lets
add some more state We need to add some parts of state to check if our library bar is open, to store our songs, to
determine what our current song is, and to determine whether a song is playing. We will do this in a similar way to our
last piece of state.

const[isLibraryOpen,setIsLibraryOpen]=useState(false);const[songs,setSongs]=useState<SongModel[]>([{color:[],id:"",active:false,artist:"",audio:"",cover:"",name:""}]);const[currentSong,setCurrentSong]=useState<SongModel>(songs[0]);const[isPlaying,setIsPlaying]=useState(false);
Enter fullscreen modeExit fullscreen mode

Now let's add another handler to skip to the next song when the current one ends.

constsongEndHandler=async()=>{letcurrIndex=songs.findIndex(song=>song.id===currentSong.id);awaitsetCurrentSong(songs[(currIndex+1)%songs.length]);if(isPlaying)awaitaudioRef.current.play();};
Enter fullscreen modeExit fullscreen mode

This async handler determines which song has just ended, and then goes to the next song. If the previous song was
playing, this next song will also play when it is loaded.

Now, we can start building our components. Go to thenav.tsx file. Our navbar will bre responsible for display and for
opening and closing our library of music. to create this element, we will use a React functional component. First, we
will need to determine what information this component needs. We do not want to give it too much access to our
application as this leads to inefficiency, but it also must be able to fulfill its purpose. Recall that we created a
part of state to track our library. Our nav component will need access to that part of state, so we will pass that in
props. To do this, we first need to create a type to inform typescript about what parameters our function will take.
Going back to ourtypes.ts file, we will create a new type.

exporttypeNavProps={isLibraryOpen:boolean,setLibraryOpen:(open:boolean)=>void}
Enter fullscreen modeExit fullscreen mode

This is a type with two parameters, the first is a normal boolean, the second indicates that it is a method which takes
in a boolean input, and returns void

Back in ourNav.tsx file, we can now create this component.

constNav=({isLibraryOpen,setLibraryOpen}:NavProps)=>{return()}
Enter fullscreen modeExit fullscreen mode

This creates our functional component which can return some babel style tsx code.

Inside the return, we will put what content we want in the navbar. In our case, a name, and a button to open and close
the library.

<nav><h1>RishikMusicPlayer</h1><buttononClick={()=>setLibraryOpen(!isLibraryOpen)}>Library</button></nav>
Enter fullscreen modeExit fullscreen mode

The button toggles the boolean in the state to open and close the library. Finally, weexport default Nav at the end
of the file so we can use this component in other places.

In theApp.tsx file, we can now add this component.

Just like we did in our nav component, we will also have a return here. In our return, we will create a div to store our
components, and then we will add our Nav component.

return(<div><NavisLibraryOpen={isLibraryOpen}setLibraryOpen={setIsLibraryOpen}/></div>)
Enter fullscreen modeExit fullscreen mode

While we're here, lets also add our audio into the application. Inside this div, lets create an audio tag which will
implement the handlers we created for it.

<audioonTimeUpdate={timeUpdateHandler}onLoadedMetadata={timeUpdateHandler}onEnded={songEndHandler}src={currentSong.audio}/>
Enter fullscreen modeExit fullscreen mode

We will also need to use reference this audio in order to use it within our application. To do this, we will use
theuseRef hook within react.

constaudioRef=useRef<HTMLAudioElement>(null!);
Enter fullscreen modeExit fullscreen mode

To link this with the audio tag, we will add a html attributeref={audioRef} on the audio tag.

We will do the Song similarly to the Nav component. We will determine what data it needs to take in, we will create a
type for those props, then we will create and export the component. For the song, all it needs to know is which song is
currently playing, so it can display the information.

We create the type for the props

exporttypeSongProps={currSong:SongModel}
Enter fullscreen modeExit fullscreen mode

Then create the component

constSong=({currSong}:SongProps)=>{return(<divclassName={"song-container"}><imgsrc={currSong.cover}alt={"album cover"}/><h2>{currSong.name}</h2><h3>{currSong.artist}</h3></div>);};
Enter fullscreen modeExit fullscreen mode

For the LibrarySong, we will need to add some more logic to set the current song when we select it in the library. We
will create another method to use the state mutator. Setting the current song is something we will do quite often, so we
can create a file with reusable methods.

Create a file calledutil.ts and let's create a function to set an active song. To do this, we will need to get the
selected song and set it to be active, and get all other songs and make sure they are inactive to ensure that we do not
have concurrent audio streams. Finally, we will update the songs state with this new information.

exportfunctionsetCurrentSongActive(song:SongModel,songs:SongModel[],setSongs:Function):void{constnewSongs=songs.map(s=>{if(s.id===song.id){return{...s,active:true,};}else{return{...s,active:false};}});setSongs(newSongs);}
Enter fullscreen modeExit fullscreen mode

We can now use this to select a song from the library.

constsongSelectHandler=async()=>{awaitsetCurrentSong(song);setCurrentSongActive(song,songs,setSongs);if(isPlaying){awaitaudioRef.play();}}
Enter fullscreen modeExit fullscreen mode

This async handler will set the current song part of the state, and then it will set the selected song as active and
play it if the previous song was also playing.

Now we can create the type and component

exporttypeLibrarySongProps={song:SongModel,songs:SongModel[],setCurrentSong:(song:SongModel)=>void,audioRef:HTMLAudioElement,isPlaying:boolean,setSongs:(songs:SongModel[])=>void}
Enter fullscreen modeExit fullscreen mode
return(<divonClick={songSelectHandler}><imgsrc={song.cover}alt={"album cover"}/><divclassName={"song-description"}><h3>{song.name}</h3><h4>{song.artist}</h4></div></div>);
Enter fullscreen modeExit fullscreen mode

Now, when we click on a song in our library, it will become selected and active. Now, lets create the library. The
library will need to display all the Library songs which we will map. We will have the props type and the component.

exporttypeLibraryProps={songs:SongModel[],setCurrentSong:(song:SongModel)=>void,audioRef:HTMLAudioElement,isPlaying:boolean,setSongs:(songs:SongModel[])=>void,isLibraryOpen:boolean,}
Enter fullscreen modeExit fullscreen mode
constLibrary=({songs,setCurrentSong,audioRef,isPlaying,setSongs,isLibraryOpen}:LibraryProps)=>{return(<div><h2>Library</h2><div>{songs.map(song=><LibrarySongsong={song}setCurrentSong={setCurrentSong}songs={songs}key={song.id}audioRef={audioRef}isPlaying={isPlaying}setSongs={setSongs}/>)}</div></div>);};
Enter fullscreen modeExit fullscreen mode

Our final component is the player. The player will have the most logic of all our components. First we need to set a
song as active from the library if we select it in the player using our next or previous buttons.

constactiveLibraryHandler=(nextPrev:SongModel)=>{constnewSongs=songs.map(s=>{if(s.id===nextPrev.id){return{...s,active:true,};}else{return{...s,active:false};}});setSongs(newSongs);};
Enter fullscreen modeExit fullscreen mode

Now we need a handler to play a song. With our isPlaying state, this is relatively easy.

constplaySongHandler=()=>{if(isPlaying){audioRef.pause();setIsPlaying(!isPlaying);}else{setIsPlaying(!isPlaying);audioRef.play();}};
Enter fullscreen modeExit fullscreen mode

Next we need to skip. We can combine forwards and backwards into one function using a switch statement. We will also
need to handle wrapping when we reach the end or beginning of our library. To do this, we can use our currentSong state
along with ouractiveLibraryHandler we created earlier.

constskipTrackHandler=async(skipDirection:string)=>{letcurrIndex=songs.findIndex(song=>song.id===currSong.id);switch(skipDirection){case'skip-back':if(currIndex===0){awaitsetCurrentSong(songs[songs.length-1]);activeLibraryHandler(songs[songs.length-1]);break;}awaitsetCurrentSong(songs[--currIndex]);activeLibraryHandler(songs[--currIndex]);break;default:awaitsetCurrentSong(songs[(currIndex+1)%songs.length]);activeLibraryHandler(songs[(currIndex+1)%songs.length]);break;}if(isPlaying)awaitaudioRef.play();};
Enter fullscreen modeExit fullscreen mode

Finally, we must handle the seeking feature. To start, we need to handle the dragging on the progress bar.

constdragHandler=(event:any)=>{audioRef.currentTime=event.target.value;setSongInfo({...songInfo,currentTime:event.target.value});};
Enter fullscreen modeExit fullscreen mode

Then, we need to format the time for display

constgetTime=(time:number):string=>{return(Math.floor(time/60)+":"+("0"+Math.floor(time%60)).slice(-2));};
Enter fullscreen modeExit fullscreen mode

This takes the time of the song and formats it in the format mm:ss

Finally, we need to fill the progress bar depending on how time has elapsed in the song.

consttrackAnimation={transform:`translateX(${songInfo.animationPercent}%)`};
Enter fullscreen modeExit fullscreen mode

Now we can return the jsx code to display our player controls.

return(<divclassName={"player"}><divclassName={"time-control"}><p>{getTime(songInfo.currentTime)}</p><divclassName={"track"}style={{background:`linear-gradient(to right,${currSong.color[0]},${currSong.color[1]}`}}><inputmin={0}max={songInfo.duration||0}value={songInfo.currentTime}type={"range"}onChange={dragHandler}/><divclassName={"animate-track"}style={trackAnimation}/></div><p>{songInfo.duration?getTime(songInfo.duration):"0:00"}</p></div><divclassName={"play-control"}><FontAwesomeIconicon={faAngleLeft}className={"back"}size={"2x"}onClick={()=>skipTrackHandler('skip-back')}/><FontAwesomeIconicon={isPlaying?faPause:faPlay}className={"play"}size={"2x"}onClick={playSongHandler}/><FontAwesomeIconicon={faAngleRight}className={"forward"}size={"2x"}onClick={()=>skipTrackHandler('skip-forward')}/></div></div>);
Enter fullscreen modeExit fullscreen mode

This logic displays the progress bar, the play/pause button depending on whether the song is currently playing, and the
skip buttons. These icons are from the FontAwesome npm package.

Now we can finally put this all together and start the styling. In ourApp.tsx file, we can add our final components
and pass in their props.

return(<div><NavisLibraryOpen={isLibraryOpen}setLibraryOpen={setIsLibraryOpen}/><SongcurrSong={currentSong}/><PlayerisPlaying={isPlaying}setIsPlaying={setIsPlaying}currSong={currentSong}audioRef={audioRef.current}songInfo={songInfo}setSongInfo={setSongInfo}songs={songs}setCurrentSong={setCurrentSong}setSongs={setSongs}/><Librarysongs={songs}setCurrentSong={setCurrentSong}audioRef={audioRef.current}isPlaying={isPlaying}setSongs={setSongs}isLibraryOpen={isLibraryOpen}/><audioonTimeUpdate={timeUpdateHandler}ref={audioRef}onLoadedMetadata={timeUpdateHandler}onEnded={songEndHandler}src={currentSong.audio}/></div>);
Enter fullscreen modeExit fullscreen mode

Styling

Now we can start styling our components.

Let's start by styling the mainApp.tsx file with ourApp.scss file. In ourApp.tsx file, we need to add classes
to our wrapping div to add in transitions and to change our main layout if the library is open. In the div, we add the
attribute
className={`App ${isLibraryOpen ? 'library-active' : ""}`} and in theapp.scss file, we will add some styling
for these classes.

.App{transition:all0.5sease;}.library-active{margin-left:30%;}
Enter fullscreen modeExit fullscreen mode

And now, we can add styling for some general types in our entire project.

*{margin:0;padding:0;box-sizing:border-box;}h1,h2,h3{color:rgb(54,54,54);}body{font-family:"Amatic SC",cursive;}h3,h4{font-weight:400;color:rgb(100,100,100);}@import"./song";@import"./player";@import"./library";@import"./nav";@importurl('https://fonts.googleapis.com/css2?family=Amatic+SC:wght@400;700&display=swap');
Enter fullscreen modeExit fullscreen mode

The final line of code allows for this project to import a non-standard font from Google Fonts. The other import
statements allow for us to pass our global styling into the other stylesheets.

Let's style library next. Similarly to the previous file, we will need to add classed depending on whether the library
is open or not. We do this with the attributeclassName={`library ${isLibraryOpen ? 'active-library': ""}`} on the
wrapping div. Now in ourlibrary.scss file,

.library{position:fixed;top:0;left:0;width:20rem;height:100%;background:white;box-shadow:2px2px50px#a2a2a2;overflow:scroll;transform:translateX(-100%);transition:all0.5sease;opacity:0;h2{padding:2rem;}}.library-song{display:flex;align-items:center;padding:1rem2rem1rem2rem;cursor:pointer;transition:background0.5sease;img{width:30%;}&:hover{background:gainsboro;}}.song-description{padding-left:1rem;h3{font-size:2rem;}h4{font-size:1rem;}}.selected{background:#aeadad;}.active-library{transform:translateX(0%);opacity:1;}*{scrollbar-width:thin;scrollbar-color:rgba(155,155,155,0.5)transparent;}*::-webkit-scrollbar{width:5px;}*::-webkit-scrollbar-track{background:transparent;}*::-webkit-scrollbar-thumb{background-color:rgba(155,155,155,0.5);border-radius:20px;border:transparent;}@mediascreenand(max-width:768px){.library{width:100%;}}
Enter fullscreen modeExit fullscreen mode

This stylesheet styles the library component and the scrollbar for both Chromium and Mozilla browsers. Additionally, on
mobile devices, the library's size is rescaled.

Next we style our navbar,

nav{min-height:10vh;display:flex;justify-content:space-around;align-items:center;button{background:transparent;cursor:pointer;border:2pxsolidrgb(65,65,65);padding:0.5rem;transition:all0.3sease;&:hover{background:rgb(65,65,65);color:white;}}}@mediascreenand(max-width:768px){nav{button{z-index:10;}}}
Enter fullscreen modeExit fullscreen mode

This styles the navbar and rescales the UI on mobile devices.

Similarly, we need to style the player:

.player{min-height:20vh;display:flex;flex-direction:column;align-items:center;justify-content:space-between;}.time-control{width:50%;display:flex;input{width:100%;-webkit-appearance:none;-moz-appearance:none;background:transparent;cursor:pointer;}p{padding:1rem;}}.play-control{display:flex;justify-content:space-between;align-items:center;padding:1rem;width:30%;svg{cursor:pointer;}}p{font-size:1.5rem;}input[type="range"]:focus{outline:none;}input[type="range"]::-webkit-slider-thumb{-webkit-appearance:none;height:16px;width:16px;background:black;}input[type="range"]::-moz-range-thumb{-moz-appearance:none;height:16px;width:16px;background:transparent;border:transparent;}.track{background:lightblue;width:100%;height:1rem;position:relative;border-radius:1rem;overflow:hidden;}.animate-track{background:rgb(204,204,204);width:100%;height:100%;position:absolute;top:0;left:0;transform:translateX(0%);pointer-events:none;}@mediascreenand(max-width:768px){.time-control{width:90%;}.play-control{width:70%;}}
Enter fullscreen modeExit fullscreen mode

This styles the progress bar on both Chromium and Mozilla browsers.

Finally, we need to style the song component,

.song-container{min-height:60vh;display:flex;flex-direction:column;align-items:center;justify-content:center;img{width:20%;border-radius:50%;}h2{padding:3rem1rem1rem1rem;font-size:3rem;}h3{font-size:2rem;}}@mediascreenand(max-width:768px){.song-container{img{width:60%;}}}
Enter fullscreen modeExit fullscreen mode

Serverless Function

Now we can get started on the creation of our http trigger to deliver our song data to our application. First, we must
create a standard Http Trigger using the Azure extension in Visual Studio Code. After we get the starter files, open
theindex.js file.

As an example I will provide a sample array of music that we can use for this tutorial. If you wish, you may provide any
music you choose that meets the same format.

constsongData=[{name:"Beaver Creek",cover:"https://chillhop.com/wp-content/uploads/2020/09/0255e8b8c74c90d4a27c594b3452b2daafae608d-1024x1024.jpg",artist:"Aso, Middle School, Aviino",audio:"https://mp3.chillhop.com/serve.php/?mp3=10075",color:["#205950","#2ab3bf"],id:uuidv4(),active:false,},{name:"Daylight",cover:"https://chillhop.com/wp-content/uploads/2020/07/ef95e219a44869318b7806e9f0f794a1f9c451e4-1024x1024.jpg",artist:"Aiguille",audio:"https://mp3.chillhop.com/serve.php/?mp3=9272",color:["#EF8EA9","#ab417f"],id:uuidv4(),active:false,},];
Enter fullscreen modeExit fullscreen mode

Insert this song array into themodule.exports section of the file. We will also need to import the uuid package to
assign ids to our songs. Finally, the function should end with thecontext.res section. Inside that section, we must
pass the songData as a part of the request.

context.res={body:songData};
Enter fullscreen modeExit fullscreen mode

This makes the body of the request include the songData array we provided.

Consuming the API call

Finally, we must put these parts together to get the songs within our application.

To do this, we must create a function to fetch from our endpoint. This function will check to see whether the data has
already been loaded, if it has not, then we will use the fetch method to return a Promise from the endpoint with the
song data we provided earlier. We then add the songs to the state.

letloaded=false;constgetSongData=async():Promise<void|SongModel[]>=>{if(loaded)return;returnawaitfetch("url").then(res=>{returnres.json()asPromise<SongModel[]>;}).then(res=>{setSongs(res);loaded=true;});};
Enter fullscreen modeExit fullscreen mode

However, we will still need to call this function as the page loads. To do this, we can use theuseEffect react hook.
TheuseEffect hook takes in a callback function, and an array of dependencies that the hook watches for. When the
dependencies update, the hook will fire. In the callback function, we need to get the data, and then we need to set the
currentSong.

useEffect(()=>{if(!loaded){getSongData();setCurrentSong(songs[0]);}},[loaded]);
Enter fullscreen modeExit fullscreen mode

My hope with this project is that if it gets deployed, it can help develop a love for smaller artists who are not big
enough to get their music on large streaming platforms. To further improve the user experience for this application, I
would like to add a way for a user to add music to the application without hardcoding it into the function. I would also
like to color more components to give the player a unique look for each album that is played.

Top comments(0)

Subscribe
pic
Create template

Templates let you quickly answer FAQs or store snippets for re-use.

Dismiss

Are you sure you want to hide this comment? It will become hidden in your post, but will still be visible via the comment'spermalink.

For further actions, you may consider blocking this person and/orreporting abuse

More fromBit Project

DEV Community

We're a place where coders share, stay up-to-date and grow their careers.

Log in Create account

[8]ページ先頭

©2009-2025 Movatter.jp