Having trouble with PWA <Audio /> and S3?
Tags: pwa, s3, nextjs, ios
Published: 2023-06-03 13:37:57

Appended info at the end of the article.

Hi all,

I'm writing a PWA which requires audio playback from S3 with presigned URLs. The presigned URLs are important because the bucket _cannot_ be public.

I wrote a little audio player, and on the web on desktop, it works every time. However, on PWA on iOS (and maybe Android, I have yet to test this), the audio playback is slow and works about half of the time. I'm curious if any of you have experimented with this, and whether you have a solution for it.

Here is the full component fwiw:
```
import { useState, useRef, useEffect } from "react";
import {
  ArrowUturnLeftIcon,
  ArrowUturnRightIcon,
  PlayIcon,
  PauseIcon,
} from "@heroicons/react/24/outline";

import { api } from "../../utils/api";
import Spinner from "../ui/Spinner";
const skipTime = 15;

const buttonStyles = `
  w-6 items-center
`;





const AudioPlayer = ({
  src,
  recordingId,
  duration
}: {
  src: string;
  recordingId: string;
  duration: number;
}) => {
  const audioRef = useRef<HTMLAudioElement>(null);
  const seekRef = useRef<HTMLInputElement>(null);
  const [_duration, setDuration] = useState(duration);
  const [currentTime, setCurrentTime] = useState(0);
  const [playing, setPlaying] = useState(false);
  const [lastTime, setLastTime] = useState(0)
  const [loaded, setLoaded] = useState(false);
  const updateLT = api.recording.updateListenTime.useMutation();
  const [audioUrl, setAudioUrl] = useState<string | null>(null);

  const downloadAudio = async () => {
    const response = await fetch(src);
    const blob = await response.blob();
    const objectUrl = URL.createObjectURL(blob);
    setAudioUrl(objectUrl);
    if (seekRef.current) {
      seekRef.current.value = "0"
    }
  }

  useEffect(() => {
    downloadAudio().then(() => {
      setLoaded(true)
      return;
    }).catch(() => {
      console.error("Could not load audio")
    })

    return () => {
      if (audioUrl) {
        URL.revokeObjectURL(audioUrl);
      }
    };
  }, [])

  return (
    <>
      {
        audioUrl &&
        <>
          <audio
            controls
            ref={audioRef}
            onDurationChange={(e) => {
              const target = e.target as HTMLAudioElement;
              const dur = target.duration;
              if (dur !== Infinity) {
                setDuration(dur)
              }
            }}
            onTimeUpdate={(e) => {
              const target = e.target as HTMLAudioElement;
              if (seekRef && seekRef.current) {
                seekRef.current.value = (target.currentTime * 1000).toString();
              }
              setCurrentTime(target.currentTime);
              target.currentTime.toString();
              if (target.currentTime - lastTime > 2) {
                setLastTime(target.currentTime);
                updateLT.mutate({
                  recordingId,
                  progress: target.currentTime,
                  duration: _duration,
                });
              }
            }}
          >
            <source src={src} type="audio/mp3" ></source>
            Your browser does not support the audio!
          </audio>
          <div
            className={`
          align-center
          rounded-md
          flex
          w-full flex-col
          justify-center
          bg-gray-200 text-gray-900
      `}
          >
            <div className={`flex h-8 w-full items-center justify-center pt-2`}>
              <span className="mx-2 w-8">
                {Math.floor(currentTime / 60).toFixed()}:
                {String((currentTime % 60).toFixed()).padStart(2, "0")}
              </span>
              <input
                type="range"
                min={0}
                max={_duration * 1000}
                ref={seekRef}
                className="h-0.5 cursor-pointer appearance-none rounded bg-white"
                onChange={(e) => {
                  const target = e.target as HTMLInputElement;
                  if (audioRef && audioRef.current) {
                    audioRef.current.currentTime = Number(target.value) / 1000;
                  }
                }}
              />
              <span className="mx-2 w-8">
                {_duration ? Math.floor(_duration / 60).toFixed() : 0}:
                {_duration
                  ? String((_duration % 60).toFixed()).padStart(2, "0")
                  : "00"}
              </span>
            </div>
            <div
              className={`
        flex w-full justify-center gap-4 pb-4
        `}
            >
              <button
                // disabled={audioContext!.tracks!.length === 0}
                className={`${buttonStyles}`}
                onClick={() => {
                  if (audioRef && audioRef.current) {
                    audioRef.current.currentTime = Math.max(
                      audioRef.current.currentTime - skipTime,
                      0
                    );
                  }
                }}
              >
                <ArrowUturnLeftIcon />
              </button>
              <button
                // disabled={audioContext!.tracks!.length === 0}
                className={`${buttonStyles}`}
                onClick={() => {
                  if (audioRef && audioRef.current) {
                    audioRef.current.volume = 1
                    if (playing) {
                      audioRef.current.pause();
                    } else {
                      audioRef.current.play().catch((e) => console.error(e));
                    }
                  }
                  setPlaying(!playing);
                }}
              >
                {loaded ? <>{playing ? <PauseIcon /> : <PlayIcon />}</> : <Spinner />}

              </button>
              <button
                // disabled={audioContext!.tracks!.length === 0}
                className={`${buttonStyles}`}
                onClick={() => {
                  if (audioRef && audioRef.current) {
                    audioRef.current.currentTime = Math.min(
                      audioRef.current.currentTime + skipTime,
                      _duration
                    );
                  }
                }}
              >
                <ArrowUturnRightIcon />
              </button>
            </div>
          </div>
        </>

      }
    </>

  );
};

export default AudioPlayer;
```

I thought if I downloaded the full file as a blob then it might work, but I'm still getting slow/no playback on iOS. This is the same behavior I got when I tried to stream directly from the S3 presigned URL.

Let me know if you have any suggestions.

Thanks

Update:

According to [this article](https://blog.prototyp.digital/what-we-learned-about-pwas-and-audio-playback/) from Prototyp, PWA audio support from Apple is _intentionally_ unreliable because of Apple's desire to maintain a monopoly on the App store (eg: forcing Spotify to use an official app instead of a PWA).

I am going to look into alternatives for audio playback, potentially a native app.