GHAYOOR
Published on

An Audio Player with react-native-video

Authors
  • avatar
    Name
    Ghayoor ul Haq
    Twitter

Why react-native-video for Audio?

There are several audio libraries like react-native-audio-api and react-native-track-player, react-native-video is a simple, all-in-one solution for handling both audio and video playback in React Native apps.

Following are some of the points that make react-native-video a great choice for audio playback over the other audio libraries:

  • Unified API: It provides a consistent API for both audio and video, making it easier to manage media playback in your app.
  • Background Playback: Handles background playback with lock screen, Control Center, and Android notification controls. You don’t need to manually manage playback event listeners like onPlay or onPause.
  • Audio Streaming: Supports streaming audio formats like HLS, providing a seamless user experience.
  • Active Maintenance: The library is actively maintained and has a large community, ensuring ongoing support and updates.

Before diving into the code, install following packages that we will be using in the example:

yarn add react-native-video @react-native-community/slider

Below is the component that uses react-native-video to play audio:

import React, { useRef, useState, useEffect } from 'react';
import { View, Text, TouchableOpacity, ActivityIndicator } from 'react-native';
import Slider from '@react-native-community/slider';
import Video from 'react-native-video';

type Status = 'idle' | 'loading' | 'playing' | 'paused' | 'error';

// Function to format seconds into a user-friendly time string
const formatSecondsToPlayerTime = (seconds: number) => {
  let timeString = '';

  // get hours, minutes, and seconds from total seconds
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const remainingSeconds = Math.floor(seconds % 60);

  // construct the time string based on whether hours are present
  if (hours > 0) {
    timeString += `${hours}:`;
  }

  // always show minutes and seconds as two digits
  if (hours > 0) {
    timeString += `${String(minutes).padStart(2, '0')}:`;
  } else {
    timeString += `${minutes}:`;
  }

  timeString += `${String(remainingSeconds).padStart(2, '0')}`;

  return timeString;
};

const AudioPlayer = () => {
  // Using useRef to hold the Video component reference
  const videoRef = useRef<Video>(null);

  // State variables to manage playback state
  const [paused, setPaused] = useState(true);
  const [duration, setDuration] = useState(0);
  const [currentTime, setCurrentTime] = useState(0);
  const [status, setStatus] = useState<Status>('idle');
  const [isSeeking, setIsSeeking] = useState(false);


  // Handle video load event to set duration and initial state
  const handleLoad = (meta: any) => {
    setDuration(meta.duration || 0);
    setStatus('paused');
  };

  // Update current time as the video plays and user is not seeking
  const handleProgress = (progress: any) => {
    if (!isSeeking) {
      setCurrentTime(progress.currentTime);
    }
  };

  // Handle sliding start to indicate user is sliding the slider
  const handleSlidingStart = () => {
    setIsSeeking(true);
  };

  // Handle sliding complete to seek the video to the new position using slider
  const handleSlidingComplete = (value: number) => {
    videoRef.current?.seek(value);
    setCurrentTime(value);
    setIsSeeking(false);
  };

  // When the audio ends, reset the player states
  const handleEnd = () => {
    setPaused(true);
    setCurrentTime(0);
    setStatus('paused');
  };

  // Handle any errors during playback
  const handleError = (error: any) => {
    console.warn('Audio playback error', error);
    setStatus('error');
  };

  // Toggle playback state between paused and playing
  const togglePlayback = () => {
    setPaused((prev) => {
      const next = !prev;
      setStatus(next ? 'paused' : 'playing');
      return next;
    });
  };

  // Seek to a specific time in the audio and update the state
  const seek = (value: number) => {
    videoRef.current?.seek(value);
    setCurrentTime(value);
    setPaused(false);
    setStatus('playing');
  };

  // Effect to update the status when paused changes
  useEffect(() => {
    if (!paused) {
      setStatus('playing');
    }
  }, [paused]);

  return (
      <View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 20 }}>
        <Video
            ref={videoRef}
            source={{
              uri: 'AUDIO_FILE_URL', // TODO: Replace with your audio file URL
              metadata: {
                title: 'React Native Audio Player',
                artist: 'ghayoor.com',
                imageUri: 'https://ghayoor.com/_next/image?url=%2Fstatic%2Fimages%2Favatar.png&w=384&q=75',
              },
            }}
            paused={paused}
            onLoad={handleLoad}
            onProgress={handleProgress}
            onEnd={handleEnd}
            onError={handleError}
            playInBackground // Allow playback in the background
            showNotificationControls // Show controls in notification area
            playWhenInactive // Allow playback when the app is inactive
            ignoreSilentSwitch="ignore"
            progressUpdateInterval={250}
        />

        <Slider
            style={{ width: '100%', marginVertical: 12 }}
            minimumValue={0}
            maximumValue={duration}
            value={currentTime}
            onSlidingStart={handleSlidingStart}
            onSlidingComplete={handleSlidingComplete}
            minimumTrackTintColor={"#1942e6"}
            maximumTrackTintColor={"#e8e8e8"}
            thumbTintColor={"#1942e6"}
        />

        <View style={{ flexDirection: 'row', justifyContent: 'space-between', width: '100%' }}>
          <Text>{formatSecondsToPlayerTime(currentTime)}</Text>
          <Text>{formatSecondsToPlayerTime(duration)}</Text>
        </View>

        <View style={{ flexDirection: 'row', marginTop: 16, alignItems: 'center' }}>
          <TouchableOpacity onPress={() => seek(Math.max(0, currentTime - 10))} style={{ marginHorizontal: 20 }}>
            <Text style={{ fontSize: 18 }}>-10s</Text>
          </TouchableOpacity>

          <TouchableOpacity onPress={togglePlayback} disabled={status === 'loading'}>
            {status === 'loading' ? (
                <ActivityIndicator color="#1C45E6" size="small" />
            ) : (
                <Text style={{ fontSize: 32 }}>{paused ? '▶️' : '⏸️'}</Text>
            )}
          </TouchableOpacity>

          <TouchableOpacity onPress={() => seek(Math.min(duration, currentTime + 10))} style={{ marginHorizontal: 20 }}>
            <Text style={{ fontSize: 18 }}>+10s</Text>
          </TouchableOpacity>
        </View>
      </View>
  );
};

export default AudioPlayer;

Explanation of the Code

  • Imports: We import necessary components from React, React Native, and the react-native-video library.
  • State Management: We use useState to manage playback state, duration, current time, and status of the audio player.
  • Video Component: The Video component from react-native-video is used to handle audio playback. We pass various props to control playback, handle events like loading, progress, end, and error.
  • Slider: We use @react-native-community/slider to allow users to scrub through the audio. It updates the current time and seeks to the new position when sliding is complete.
  • Playback Controls: We provide buttons to play/pause the audio and seek backward or forward by 10 seconds. The playback state is toggled using the togglePlayback function.
  • Formatting Time: The formatSecondsToPlayerTime function formats the current time and duration into a player-friendly string format.
  • Background Playback: The playInBackground and showNotificationControls props allow the audio to continue playing when the app is in the background, with controls available in the notification area.

Usage

Change the AUDIO_FILE_URL in the source prop of the Video component to the URL of your audio file. This can be a remote URL or a local file path. Although we added playInBackground and showNotificationControls to Video component, you still need to configure additional settings in your app for background audio playback, especially on iOS. This typically involves adding Background Modes in your Xcode project settings and enabling Audio, AirPlay, and Picture in Picture.

To use this AudioPlayer component, simply import it into your main application file (e.g., App.tsx) and include it in your component tree:

import React from 'react';
import { SafeAreaView } from 'react-native';
import AudioPlayer from './AudioPlayer'; // Adjust the path as necessary
const App = () => {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <AudioPlayer />
    </SafeAreaView>
  );
};

export default App;

Note: At the time of writing this blog, react-native-video is at version 6.16.0

Following are screenshots of the audio player in action:

Audio Player

Control Center

Lock Screen