DEV Community

Cover image for How We Developed an Online Music Player with TypeScript
Upsilon
Upsilon

Posted on • Updated on

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.


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.

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.

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 a programming code that could be written for a similar project.

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<AudioState>) => {
    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);
    },
  };
};
Enter fullscreen mode Exit fullscreen mode

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 = { ...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;
Enter fullscreen mode Exit fullscreen mode

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

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

import player from './player';

const usePlayerState = () => {
  const [state, setState] = useState(player.getState());

  useEffect(() => {
    const unsubscribe = player.subscribe(setState);

    return unsubscribe;
  }, []);

  return state;
};

const Player: FC = () => {
  const { currentTrack, playing } = usePlayerState();

  useEffect(() => {
    player.setQueue([
      {
        title: 'Tech House Vibes',
        url: '/music/mixkit-a-very-happy-christmas-897.mp3',
      },
      {
        title: 'Sample Video',
        url: '/videos/sample-video.mp4',
      },
    ]);
  }, []);

  const handlePlay = () => {
    if (playing) {
      player.pause();
    } else {
      player.play();
    }
  };

  return (
    <div>
      <button onClick={player.prev} disabled={!currentTrack}>
        Prev
      </button>

      {currentTrack ? (
        <button onClick={handlePlay}>{playing ? 'Pause' : 'Play'}</button>
      ) : (
        <button onClick={() => player.playTrack(0)}>Play All</button>
      )}

      <button onClick={player.next} disabled={!currentTrack}>
        Next
      </button>
    </div>
  );
};
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode
import React, { useRef, useEffect, FC } from 'react';

import player from './player';

const Video: FC = () => {
  const videoWrapperRef = useRef<HTMLDivElement>(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 <div ref={videoWrapperRef} />;
};
Enter fullscreen mode Exit fullscreen mode

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.

On the CodeSandbox ou can explore code examples in more detail.

After performing the steps above, we created a music player that further became the essential component of the client’s Social Music Web Platform.


Have questions? We welcome any feedback and will be glad to start a discussion in the comments section.

Top comments (0)