diff --git a/www/app/[domain]/transcripts/[transcriptId]/page.tsx b/www/app/[domain]/transcripts/[transcriptId]/page.tsx index 3668ba5f..56201c3c 100644 --- a/www/app/[domain]/transcripts/[transcriptId]/page.tsx +++ b/www/app/[domain]/transcripts/[transcriptId]/page.tsx @@ -68,7 +68,8 @@ export default function TranscriptDetails(details: TranscriptDetails) { waveform={waveform?.waveform} isPastMeeting={true} transcriptId={transcript?.response?.id} - mp3Blob={mp3.blob} + media={mp3?.media} + mediaDuration={transcript?.response?.duration} /> )} diff --git a/www/app/[domain]/transcripts/recorder.tsx b/www/app/[domain]/transcripts/recorder.tsx index 77026a83..8db32ff7 100644 --- a/www/app/[domain]/transcripts/recorder.tsx +++ b/www/app/[domain]/transcripts/recorder.tsx @@ -29,7 +29,8 @@ type RecorderProps = { waveform?: AudioWaveform | null; isPastMeeting: boolean; transcriptId?: string | null; - mp3Blob?: Blob | null; + media?: HTMLMediaElement | null; + mediaDuration?: number | null; }; export default function Recorder(props: RecorderProps) { @@ -103,6 +104,11 @@ export default function Recorder(props: RecorderProps) { // Waveform setup useEffect(() => { if (waveformRef.current) { + // XXX duration is required to prevent recomputing peaks from audio + // However, the current waveform returns only the peaks, and no duration + // And the backend does not save duration properly. + // So at the moment, we deduct the duration from the topics. + // This is not ideal, but it works for now. const _wavesurfer = WaveSurfer.create({ container: waveformRef.current, peaks: props.waveform?.data, @@ -110,6 +116,7 @@ export default function Recorder(props: RecorderProps) { autoCenter: true, barWidth: 2, height: "auto", + duration: props.mediaDuration || 1, ...waveSurferStyles.player, }); @@ -139,8 +146,8 @@ export default function Recorder(props: RecorderProps) { if (props.isPastMeeting) _wavesurfer.toggleInteraction(true); - if (props.mp3Blob) { - _wavesurfer.loadBlob(props.mp3Blob); + if (props.media) { + _wavesurfer.setMediaElement(props.media); } setWavesurfer(_wavesurfer); @@ -156,9 +163,9 @@ export default function Recorder(props: RecorderProps) { useEffect(() => { if (!wavesurfer) return; - if (!props.mp3Blob) return; - wavesurfer.loadBlob(props.mp3Blob); - }, [props.mp3Blob, wavesurfer]); + if (!props.media) return; + wavesurfer.setMediaElement(props.media); + }, [props.media, wavesurfer]); useEffect(() => { topicsRef.current = props.topics; @@ -282,8 +289,6 @@ export default function Recorder(props: RecorderProps) { if (!record) return; if (destinationStream !== null) return console.log("already recording"); - console.log("startTabRecording"); - // connect mic audio (microphone) const micStream = await getCurrentStream(); if (!micStream) { diff --git a/www/app/[domain]/transcripts/useMp3.ts b/www/app/[domain]/transcripts/useMp3.ts index c9506273..570a6a25 100644 --- a/www/app/[domain]/transcripts/useMp3.ts +++ b/www/app/[domain]/transcripts/useMp3.ts @@ -7,74 +7,59 @@ import { shouldShowError } from "../../lib/errorUtils"; type Mp3Response = { url: string | null; - blob: Blob | null; + media: HTMLMediaElement | null; loading: boolean; error: Error | null; }; const useMp3 = (protectedPath: boolean, id: string): Mp3Response => { const [url, setUrl] = useState(null); - const [blob, setBlob] = useState(null); + const [media, setMedia] = useState(null); const [loading, setLoading] = useState(false); const [error, setErrorState] = useState(null); const { setError } = useError(); const api = getApi(protectedPath); const { api_url } = useContext(DomainContext); const accessTokenInfo = useFiefAccessTokenInfo(); + const [serviceWorkerReady, setServiceWorkerReady] = useState(false); + + useEffect(() => { + if ("serviceWorker" in navigator) { + navigator.serviceWorker.register("/service-worker.js").then(() => { + setServiceWorkerReady(true); + }); + } + }, []); + + useEffect(() => { + if (!navigator.serviceWorker) return; + if (!navigator.serviceWorker.controller) return; + if (!serviceWorkerReady) return; + // Send the token to the service worker + navigator.serviceWorker.controller.postMessage({ + type: "SET_AUTH_TOKEN", + token: accessTokenInfo?.access_token, + }); + }, [navigator.serviceWorker, serviceWorkerReady, accessTokenInfo]); const getMp3 = (id: string) => { if (!id || !api) return; + // createa a audio element and set the source setLoading(true); - // XXX Current API interface does not output a blob, we need to to is manually - // const requestParameters: V1TranscriptGetAudioMp3Request = { - // transcriptId: id, - // }; - // api - // .v1TranscriptGetAudioMp3(requestParameters) - // .then((result) => { - // setUrl(result); - // setLoading(false); - // console.debug("Transcript Mp3 loaded:", result); - // }) - // .catch((err) => { - // setError(err); - // setErrorState(err); - // }); - const localUrl = `${api_url}/v1/transcripts/${id}/audio/mp3`; - if (localUrl == url) return; - const headers = new Headers(); - - if (accessTokenInfo) { - headers.set("Authorization", "Bearer " + accessTokenInfo.access_token); - } - fetch(localUrl, { - method: "GET", - headers, - }) - .then((response) => { - setUrl(localUrl); - response.blob().then((blob) => { - setBlob(blob); - setLoading(false); - }); - }) - .catch((err) => { - setErrorState(err); - const shouldShowHuman = shouldShowError(error); - if (shouldShowHuman) { - setError(err, "There was an error loading the audio"); - } else { - setError(err); - } - }); + const audioElement = document.createElement("audio"); + audioElement.src = `${api_url}/v1/transcripts/${id}/audio/mp3`; + audioElement.crossOrigin = "anonymous"; + audioElement.preload = "auto"; + setMedia(audioElement); + setLoading(false); }; useEffect(() => { getMp3(id); }, [id, api]); - return { url, blob, loading, error }; + return { url, media, loading, error }; }; export default useMp3; diff --git a/www/public/service-worker.js b/www/public/service-worker.js new file mode 100644 index 00000000..109561d5 --- /dev/null +++ b/www/public/service-worker.js @@ -0,0 +1,25 @@ +let authToken = ""; // Variable to store the token + +self.addEventListener("message", (event) => { + if (event.data && event.data.type === "SET_AUTH_TOKEN") { + authToken = event.data.token; + } +}); + +self.addEventListener("fetch", function (event) { + // Check if the request is for a media file + if (/\/v1\/transcripts\/.*\/audio\/mp3$/.test(event.request.url)) { + // Modify the request to add the Authorization header + const modifiedHeaders = new Headers(event.request.headers); + if (authToken) { + modifiedHeaders.append("Authorization", `Bearer ${authToken}`); + } + + const modifiedRequest = new Request(event.request, { + headers: modifiedHeaders, + }); + + // Respond with the modified request + event.respondWith(fetch(modifiedRequest)); + } +});