reactjsblobvitehtml5-audiowavesurfer.js

Fast loading of audio WaveSurfer and PeakJs


I have this implementation where the backend strams audio files. And I want an approach that enables fast loading and playing of the audio file : Everything works as intended but there seem to be erratic behaviour where it takes very long time to load on my web page:

I am new to React Js .

import React, { useState,useEffect, useRef } from 'react';
import { Icon } from '@iconify/react';
import { podcastIcons } from '@/db/data';
import axios from 'axios';
import WaveSurfer from 'wavesurfer.js';


const PlayFile: React.FC = () => {
    const [linkName, setLinkName] = useState<string>(''); // State for link name
    
    
    const [audioURL, setAudioURL] = useState<string | null>(null);
    const [isLoading, setIsLoading] = useState(false);
    const waveformRef = useRef<HTMLDivElement | null>(null); // Reference to the waveform container
    const waveSurfer = useRef<WaveSurfer | null>(null); // Reference to the WaveSurfer instance
    
    const handleSubmit = async (event: React.FormEvent) => {
        event.preventDefault();

        const formData = new FormData();
        formData.append('link_name', linkName);
        try {
                        const response = await axios.post('http://localhost:8000/submitform/', formData, {
                            headers: {
                                'Content-Type': 'multipart/form-data',
                            },
                        });
            const audioBlob = new Blob([response.data], { type: 'audio/mpeg' });
            const audioUrl = URL.createObjectURL(audioBlob);
            setAudioURL(audioUrl); // Set audio UR
            

            // Initialize Wavesurfer after the audio is set
            console.log('checking waveform');
            if (waveformRef.current && waveSurfer.current === null) {
                waveSurfer.current = WaveSurfer.create({
                    container: waveformRef.current,
                    waveColor: '#ddd',
                    progressColor: '#333',
                    cursorColor: '#333',
                    height: 64,
                    barWidth: 1,
                    responsive: true,
                   // backend: 'MediaElement'
                });
            }

            // Load the audio file into Wavesurfer
            console.log('after checking ...');
            if (waveSurfer.current && audioUrl) {
                console.log('Waveform loaded with audio URL:', audioUrl);
                waveSurfer.current.load(audioUrl);
            }
            //    } catch (error) {
            // console.error('Error generating the podcast:', error);
            //     } 
                    } catch (error) {
                        console.error('Error submitting the form', error);
                    }finally {
            setIsLoading(false);
              }
    };
  
);
return (

        <form onSubmit={handleSubmit} className=" bg-gradient-to-r from-emerald-100 to-lime-200 flex text-black flex-col lg:flex-row lg:space-x-8 p-6 font-sans py-12 px-6">

         
                </div>
                 
                {/* Link Name Input Field */}
                <div className="mb-6">
                    <label className="font-semibold mb-2">Document Link</label>
                    <input
                        type="Document link"
                        value={linkName}
                        onChange={(e) => setLinkName(e.target.value)}
                        className="border rounded w-full py-2 px-3"
                        placeholder="Link to Document"
                    />
                </div>
                <button className="bg-black text-white py-3 px-8 rounded w-full">
                    Create podcast
                </button>
           
            {/* Show the audio player and waveform if the audio URL is available */}
            {audioURL && (
                <div className="mt-4">
                    <div className="waveform" ref={waveformRef}></div> {/* Waveform container */}
                    <div className="mt-2 flex gap-4">
                        <button onClick={() => waveSurfer.current?.playPause()} className="bg-green-500 text-white py-2 px-4 rounded">
                            {waveSurfer.current?.isPlaying() ? 'Pause' : 'Play'}
                        </button>
                        <button onClick={() => waveSurfer.current?.stop()} className="bg-red-500 text-white py-2 px-4 rounded">
                            Stop
                        </button>
                     
                    </div>
                </div>
            )}

            </div>
        </form>
    );
};

export default PlayFile;

I have tried the code above and it works sometimes and other times it does not. I am looking for wats of optimizing the code and gaining hands-on-experience in handling audio rendering on frontnd in a scalable way. Examples illustrating best practices will be well appreciated. SOlution using peaks.js, howler and other similar libraries that are optimized for performance is also welcomed.


Solution

  • Adopting Howler.js in your React application offers numerous advantages that directly address the challenges you’re facing with your current implementation:

    1. Performance Improvements: Efficient handling of large audio files through streaming reduces load times and memory usage.
    2. Simpler and More Reliable Code: A straightforward API minimizes complexity, making your application more maintainable and less prone to bugs.
    3. Better User Experience: Faster playback initiation and responsive controls enhance the overall user experience.
    4. Robust Resource Management: Proper cleanup and error handling ensure your application remains stable and performant over time.
    5. Cross-Browser Consistency: Howler.js abstracts away browser inconsistencies, providing a uniform experience across different environments.

    By leveraging Howler.js, you can create a scalable, efficient, and user-friendly audio playback feature in your React application, resolving the erratic loading behaviors and enhancing overall performance.

    Try this code:

    If you later decide to incorporate advanced features like waveform visualization, you can integrate specialized libraries (e.g., WaveSurfer.js) alongside Howler.js, maintaining a modular and efficient architecture.

    import React, { useState, useRef, useEffect } from 'react';
    import axios from 'axios';
    import { Howl } from 'howler';
    
    const PlayFileWithHowler = () => {
        const [linkName, setLinkName] = useState('');
        const [isLoading, setIsLoading] = useState(false);
        const [howl, setHowl] = useState(null);
        const [isPlaying, setIsPlaying] = useState(false);
    
        const handleSubmit = async (event) => {
            event.preventDefault();
            setIsLoading(true);
    
            const formData = new FormData();
            formData.append('link_name', linkName);
    
            try {
                const response = await axios.post('http://localhost:8000/submitform/', formData, {
                    headers: {
                        'Content-Type': 'multipart/form-data',
                    },
                    responseType: 'blob', // Ensure the response is a blob
                });
    
                const audioBlob = new Blob([response.data], { type: 'audio/mpeg' });
                const audioUrl = URL.createObjectURL(audioBlob);
    
                const newHowl = new Howl({
                    src: [audioUrl],
                    html5: true, // Use HTML5 Audio to enable streaming large files
                    onend: () => setIsPlaying(false),
                    onplay: () => setIsPlaying(true),
                    onpause: () => setIsPlaying(false),
                    onstop: () => setIsPlaying(false),
                    onloaderror: (id, error) => console.error('Load error:', error),
                    onplayerror: (id, error) => console.error('Play error:', error),
                });
    
                setHowl(newHowl);
            } catch (error) {
                console.error('Error submitting the form', error);
                // Optionally, set an error state here to display to the user
            } finally {
                setIsLoading(false);
            }
        };
    
        const handlePlayPause = () => {
            if (howl) {
                howl.playing() ? howl.pause() : howl.play();
            }
        };
    
        const handleStop = () => {
            if (howl) {
                howl.stop();
            }
        };
    
        // Cleanup Howl on unmount
        useEffect(() => {
            return () => {
                if (howl) {
                    howl.unload();
                }
            };
        }, [howl]);
    
        return (
            <form onSubmit={handleSubmit} className="bg-gradient-to-r from-emerald-100 to-lime-200 flex flex-col lg:flex-row lg:space-x-8 p-6 font-sans py-12 px-6">
                {/* Link Name Input Field */}
                <div className="mb-6 flex-1">
                    <label className="font-semibold mb-2 block">Document Link</label>
                    <input
                        type="text"
                        value={linkName}
                        onChange={(e) => setLinkName(e.target.value)}
                        className="border rounded w-full py-2 px-3"
                        placeholder="Link to Document"
                        required
                    />
                </div>
                <div className="flex items-end">
                    <button
                        type="submit"
                        className="bg-black text-white py-3 px-8 rounded w-full"
                        disabled={isLoading}
                    >
                        {isLoading ? 'Loading...' : 'Create Podcast'}
                    </button>
                </div>
    
                {/* Show the audio controls if Howl is initialized */}
                {howl && (
                    <div className="mt-4 w-full">
                        <div className="flex gap-4">
                            <button
                                type="button"
                                onClick={handlePlayPause}
                                className="bg-green-500 text-white py-2 px-4 rounded"
                            >
                                {isPlaying ? 'Pause' : 'Play'}
                            </button>
                            <button
                                type="button"
                                onClick={handleStop}
                                className="bg-red-500 text-white py-2 px-4 rounded"
                            >
                                Stop
                            </button>
                        </div>
                    </div>
                )}
            </form>
        );
    };
    
    export default PlayFileWithHowler;