How We Developed an Online Music Player with TypeScript

In this article, we share our experience of creating a custom music player for a Social Music Web Platform with TypeScript illustrating each step with a programming code.

January 28, 2021

4 min read

Technologies

One of our clients, a Music Tech Company, came with a request to develop a web version of their Social Music Platform. They wanted to have an online music player that would provide advanced functionality and responsive design.

After interviewing the client and analyzing the requirements and documentation attached, we started to look for the solutions. We saw that most of them are over-complicated; therefore, we decided to develop a custom music player.

Disclaimer: in this article, we present code examples (not currently executing one)! Here, we would like to share our experience of creating a custom music player with TypeScript illustrating each step with programming code that could be written for a similar project.

Technology Used to Develop the Custom Music Player

Let's go over the tech stack that we used to create the custom music player. To deliver all player's features according to the requirements, we built the music player's core functionality on TypeScript

For UI development, we used React, Next.js, Redux-Toolkit, Material-UI.

For custom styling of Material-UI components, we used the CSS-in-JS approach.

Custom Music Player Development: Step-by-Step Guide

Step 1: Split the Project into Modules

Firstly, we split the project into modules. One of the modules works with the logic of track playback: play/pause, track length displaying, next/previous, volume control, etc. Also, please note that we used the publish-subscribe pattern and wrote a small implementation - pubsub.ts.


import { createPubSub } from './pubsub';

type AudioState = {
  duration: number;
  playing: boolean;
  volume: number;
};

export const createAudio = () => {
  const pubsub = createPubSub();
  const element = document.createElement('video');
  let currentTime = 0;

  let state: AudioState = {
    duration: 0,
    playing: false,
    volume: 0,
  };

  const setState = (value: Partial) => {
    state = { ...state, ...value };

    pubsub.publish('change', state);
  };

  const setup = () => {
    element.addEventListener('durationchange', () =>
      setState({ duration: element.duration }),
    );

    element.addEventListener('playing', () => setState({ playing: true }));

    element.addEventListener('pause', () => setState({ playing: false }));

    element.addEventListener('timeupdate', () => {
      const newCurrentTime = Math.round(element.currentTime);

      if (currentTime !== newCurrentTime) {
        currentTime = newCurrentTime;

        pubsub.publish('change-current-time', currentTime);
      }
    });

    element.addEventListener('volumechange', () =>
      setState({ volume: element.volume }),
    );

    setState({ volume: element.volume });
  };

  setup();

  return {
    seek(seconds: number) {
      element.currentTime = seconds;
      currentTime = seconds;

      pubsub.publish('change-current-time', currentTime);
    },

    getElement() {
      return element;
    },

    getState() {
      return state;
    },

    getCurrentTime() {
      return currentTime;
    },

    play() {
      element.play();
    },

    pause() {
      element.pause();
    },

    volume(value: number) {
      element.volume = value;
    },

    setUrl(url: string) {
      element.setAttribute('src', url);
      setState({ playing: false });
    },

    subscribe(listener: (newState: AudioState) => void) {
      return pubsub.subscribe('change', listener);
    },

    onChangeCurrentTime(listener: (newCurrentTime: number) => void) {
      return pubsub.subscribe('change-current-time', listener);
    },

    onEnded(listener: () => void) {
      element.addEventListener('ended', listener);

      return () => element.removeEventListener('ended', listener);
    },
  };
};

audio.ts

Another module includes the functionality of the track playback and playlists management. See the code example below:


import { createPubSub } from './pubsub';
import { createAudio } from './audio';

type Track = {
  url: string;
  title: string;
};

type State = AudioState & {
  tracks: Track[];
  currentTrack: Track | null;
  currentTrackIndex: number | null;
};

const createPlayer = () => {
  const pubsub = createPubSub();
  const audio = createAudio();

  let state: State = {
    ...audio.getState(),
    tracks: [],
    currentTrackIndex: null,
    currentTrack: null,
  };

  const setState = (value: Partial) => {
    state = { ...state, ...value };

    pubsub.publish('change', state);
  };

  audio.subscribe(setState);

  const changeTrack = () => {
    const track = state.currentTrack;

    if (track) {
      audio.setUrl(track.url);
      audio.play();
    }
  };

  const next = () => {
    if (state.currentTrackIndex === null) {
      return;
    }

    const lastIndex = state.tracks.length - 1;
    const newIndex = state.currentTrackIndex + 1;

    if (newIndex <= lastIndex) {
      setState({
        currentTrackIndex: newIndex,
        currentTrack: state.tracks[newIndex],
      });

      changeTrack();
    }
  };

  audio.onEnded(next);

  return {
    play: audio.play,
    pause: audio.pause,
    seek: audio.seek,
    volume: audio.volume,
    getCurrentTime: audio.getCurrentTime,
    getElement: audio.getElement,
    onChangeCurrentTime: audio.onChangeCurrentTime,

    getState() {
      return state;
    },

    setQueue(tracks: Track[]) {
      setState({ tracks });
    },

    playTrack(trackIndex: number) {
      setState({
        currentTrackIndex: trackIndex,
        currentTrack: state.tracks[trackIndex],
      });

      changeTrack();
    },

    next,

    prev() {
      if (state.currentTrackIndex === null) {
        return;
      }

      const newIndex = state.currentTrackIndex - 1;

      if (newIndex >= 0) {
        setState({
          currentTrack: state.tracks[newIndex],
          currentTrackIndex: newIndex,
        });

        changeTrack();
      }
    },

    subscribe(listener: (newState: State) => void) {
      return pubsub.subscribe('change', listener);
    },
  };
};

const player = createPlayer();

export default player;

player.ts

Step 2: React Component Usage

Here are several lines of code illustrating the React component that uses the player functionality shown above:




Step 3: Background Mode for Video Files

And here is an example of how a special player’s background mode for video files can be written. In that mode, we can move from one screen (or page) to another, and the video continues to play without any interference.


.visuallyhidden {
  position: absolute;
  overflow: hidden;
  clip: rect(0 0 0 0);
  height: 1;
  width: 1;
  margin: -1;
  padding: 0;
  border: 0;
}


import React, { useRef, useEffect, FC } from 'react';

import player from './player';

const Video: FC = () => {
  const videoWrapperRef = useRef(null);

  useEffect(() => {
    const videoWrapperElement = videoWrapperRef.current!;
    const video = player.getElement();

    video.remove();
    video.classList.remove('visuallyhidden');
    videoWrapperElement.append(video);

    return () => {
      videoWrapperElement.removeChild(video);
      video.classList.add('visuallyhidden');
      document.body.append(video);
    };
  }, []);

  return 
; };

In this example, you can see: when a user switches the player from background to foreground mode, the video expands back, and the audio track continues to play ‘seamlessly’ with no pauses.

Concluding Thoughts

After performing the steps above, we created a music player that further became the essential component of the client’s Social Music Web Platform. You can learn more about it in the corresponding "Social Music Platform" case study and explore more code examples in the CodeSandbox in more detail.

If you have any questions regarding our custom software development services, don't hesitate to contact us and discuss you project ideas!

Technologies

Join the newsletter to receive the latest updates in your inbox.

Awesome!

Now you will definitely not miss anything.
Oops! Something went wrong while submitting the form.

Join the newsletter to receive the latest updates in your inbox.

Awesome!

Now you will definitely not miss anything.
Oops! Something went wrong while submitting the form.
Featured posts
Authors
scroll
to top