import React from "react"; import WaveSurfer from "wavesurfer.js"; import { useEffect, useRef, useState } from "react"; import { Icon, P } from "@jambonz/ui-kit"; import { Icons, ModalClose } from "src/components"; import { getBlob, getJaegerTrace } from "src/api"; import { DownloadedBlob, RecentCall } from "src/api/types"; import RegionsPlugin, { Region } from "wavesurfer.js/src/plugin/regions"; import TimelinePlugin from "wavesurfer.js/src/plugin/timeline"; import { API_BASE_URL } from "src/api/constants"; import { JaegerRoot, JaegerSpan, WaveSufferDtmfResult, WaveSufferSttResult, } from "src/api/jaeger-types"; import { getSpanAttributeByName, getSpansByName, getSpansByNameRegex, getSpansFromJaegerRoot, } from "./utils"; type PlayerProps = { call: RecentCall; }; export const Player = ({ call }: PlayerProps) => { const { recording_url, call_sid } = call; const url = recording_url && recording_url.startsWith("http://") ? recording_url : `${API_BASE_URL}${recording_url}`; const JUMP_DURATION = 15; //seconds const [isPlaying, setIsPlaying] = useState(false); const [isReady, setIsReady] = useState(false); const [playBackTime, setPlayBackTime] = useState(""); const [jaegerRoot, setJeagerRoot] = useState(); const [waveSufferRegionData, setWaveSufferRegionData] = useState(); const [waveSufferDtmfData, setWaveSufferDtmfData] = useState(); const [regionChecked, setRegionChecked] = useState(false); const wavesurferId = `wavesurfer--${call_sid}`; const wavesurferTimelineId = `timeline-${wavesurferId}`; const waveSufferRef = useRef(null); const [record, setRecord] = useState(null); const drawDtmfRegionForSpan = (s: JaegerSpan, startPoint: JaegerSpan) => { if (waveSufferRef.current) { const r = waveSufferRef.current.regions.list[s.spanId]; if (!r) { const [dtmfValue] = getSpanAttributeByName(s.attributes, "dtmf"); const [durationValue] = getSpanAttributeByName( s.attributes, "duration" ); if (dtmfValue && durationValue) { const start = (s.startTimeUnixNano - startPoint.startTimeUnixNano) / 1_000_000_000; const duration = Number(durationValue.value.stringValue.replace("ms", "")) / 1_000; // as duration of DTMF is short, cannot be shown in wavesuffer, // adjust region width here. const delta = duration <= 0.1 ? 0.1 : duration; const end = start + delta; const region = waveSufferRef.current.addRegion({ id: s.spanId, start, end, color: "rgba(138, 43, 226, 0.15)", drag: false, loop: false, resize: false, }); changeRegionMouseStyle(region); const att: WaveSufferDtmfResult = { dtmf: dtmfValue.value.stringValue, duration: durationValue.value.stringValue, }; region.on("click", () => { setWaveSufferDtmfData(att); }); } } } }; const changeRegionMouseStyle = (region: Region, channel = 0) => { region.element.style.display = regionChecked ? "" : "none"; region.element.style.height = "49%"; region.element.style.top = channel === 0 ? "0" : "51%"; region.element.addEventListener("mouseenter", () => { region.element.style.cursor = "pointer"; // Change to your desired cursor style }); region.element.addEventListener("mouseleave", () => { region.element.style.cursor = "default"; }); }; const drawSttRegionForSpan = ( s: JaegerSpan, startPoint: JaegerSpan, channel = 0 ) => { if (waveSufferRef.current) { const r = waveSufferRef.current.regions.list[s.spanId]; if (!r) { const start = (s.startTimeUnixNano - startPoint.startTimeUnixNano) / 1_000_000_000 + 0.05; // add magic 0.01 second in each region start time to isolate 2 near regions const end = (s.endTimeUnixNano - startPoint.startTimeUnixNano) / 1_000_000_000; const region = waveSufferRef.current.addRegion({ id: s.spanId, start, end, color: "rgba(255, 0, 0, 0.15)", drag: false, loop: false, resize: false, }); changeRegionMouseStyle(region, channel); const [sttResult] = getSpanAttributeByName(s.attributes, "stt.result"); let att: WaveSufferSttResult; if (sttResult) { const data = JSON.parse(sttResult.value.stringValue); att = { vendor: data.vendor.name, transcript: data.alternatives[0].transcript, confidence: data.alternatives[0].confidence, language_code: data.language_code, }; } else { const [sttResolve] = getSpanAttributeByName( s.attributes, "stt.resolve" ); if (sttResolve && sttResolve.value.stringValue === "timeout") { att = { vendor: "", transcript: "None (speech session timeout)", confidence: 0, language_code: "", }; } else { att = { vendor: "", transcript: "None (call disconnected or speech session terminated)", confidence: 0, language_code: "", }; } } region.on("click", () => { setWaveSufferRegionData(att); }); } } }; const buildWavesufferRegion = () => { if (jaegerRoot) { const spans = getSpansFromJaegerRoot(jaegerRoot); const [startPoint] = getSpansByName(spans, "background-listen:listen"); // there should be only one startPoint for background listen if (startPoint) { const gatherSpans = getSpansByNameRegex(spans, /:gather{/); gatherSpans.forEach((s) => { drawSttRegionForSpan(s, startPoint); }); const transcribeSpans = getSpansByNameRegex(spans, /stt-listen:/); transcribeSpans.forEach((cs) => { // Channel start from 0 const channel = Number(cs.name.split(":")[1]); drawSttRegionForSpan( cs, startPoint, channel > 0 ? channel - 1 : channel ); }); const dtmfSpans = getSpansByNameRegex(spans, /dtmf:/); dtmfSpans.forEach((ds) => { drawDtmfRegionForSpan(ds, startPoint); }); } } }; useEffect(() => { buildWavesufferRegion(); }, [jaegerRoot, isReady]); useEffect(() => { getBlob(url).then(({ blob, headers }) => { if (blob) { const ext = headers.get("Content-Type") === "audio/wav" ? "wav" : "mp3"; setRecord({ data_url: URL.createObjectURL(blob), file_name: `callid-${call_sid}.${ext}`, }); } }); if (call.trace_id && call.trace_id != "00000000000000000000000000000000") { getJaegerTrace(call.account_sid, call.trace_id).then(({ json }) => { if (json) { setJeagerRoot(json); } }); } }, []); function formatTime(seconds: number) { const minutes = Math.floor(seconds / 60); const remainingSeconds = Math.floor(seconds % 60); return `${minutes.toString().padStart(2, "0")}:${remainingSeconds .toString() .padStart(2, "0")}`; } useEffect(() => { if (waveSufferRef.current !== null || !record) return; waveSufferRef.current = WaveSurfer.create({ container: `#${wavesurferId}`, waveColor: "#da1c5c", progressColor: "grey", height: 50, cursorWidth: 1, cursorColor: "lightgray", normalize: true, responsive: true, fillParent: true, splitChannels: true, scrollParent: true, plugins: [ RegionsPlugin.create({}), TimelinePlugin.create({ container: `#${wavesurferTimelineId}`, }), ], }); waveSufferRef.current.load(record?.data_url); // All event should be after load waveSufferRef.current.on("finish", () => { setIsPlaying(false); }); waveSufferRef.current.on("play", () => { setIsPlaying(true); }); waveSufferRef.current.on("pause", () => { setIsPlaying(false); }); waveSufferRef.current.on("ready", () => { setIsReady(true); setPlayBackTime(formatTime(waveSufferRef.current?.getDuration() || 0)); }); waveSufferRef.current.on("audioprocess", () => { setPlayBackTime(formatTime(waveSufferRef.current?.getCurrentTime() || 0)); }); }, [record]); const togglePlayback = () => { if (waveSufferRef.current) { if (!isPlaying) { waveSufferRef.current.play(); } else { waveSufferRef.current.pause(); } } }; const setPlaybackJump = (delta: number) => { if (waveSufferRef.current) { const idx = waveSufferRef.current.getCurrentTime() + delta; const value = idx <= 0 ? 0 : idx >= waveSufferRef.current.getDuration() ? waveSufferRef.current.getDuration() - 1 : idx; waveSufferRef.current.setCurrentTime(value); setPlayBackTime(formatTime(value)); } }; if (!record) return null; return ( <>
{playBackTime}
{waveSufferRegionData && ( setWaveSufferRegionData(null)}>

Speech to text result

{waveSufferRegionData.vendor && (
Vendor:
{waveSufferRegionData.vendor}
)} {waveSufferRegionData.confidence !== 0 && (
Confidence:
{waveSufferRegionData.confidence}
)} {waveSufferRegionData.language_code && (
Language code:
{waveSufferRegionData.language_code}
)} {waveSufferRegionData.transcript && (
Transcript:
{waveSufferRegionData.transcript}
)}
)} {waveSufferDtmfData && ( setWaveSufferDtmfData(null)}>

Dtmf result

Dtmf:
{waveSufferDtmfData.dtmf}
Duration:
{waveSufferDtmfData.duration}
)} ); };