react-edge-tts
Version:
Generate text-to-speech narration for React content using Microsoft Edge TTS
131 lines • 6.99 kB
JavaScript
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { Play, Pause, Volume2, Volume1, VolumeX } from 'lucide-react';
import '../styles/NarrationPlayer.css';
const defaultTheme = {
primary: '#2563eb',
secondary: '#94a3b8',
background: '#f8fafc',
text: '#1e293b',
};
const formatTime = (seconds) => {
const mins = Math.floor(seconds / 60);
const secs = Math.floor(seconds % 60);
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
export const NarrationPlayer = ({ title, src, className = '', style = {}, showTime = true, showVolume = true, defaultVolume = 1, theme = defaultTheme, metadata, onPlay, onPause, onEnded, onError, }) => {
const audioRef = useRef(null);
const [isPlaying, setIsPlaying] = useState(false);
const [currentTime, setCurrentTime] = useState(0);
const [duration, setDuration] = useState(0);
const [volume, setVolume] = useState(defaultVolume);
const audioSrc = src || `narration-audio/${title}.mp3`;
// MediaSession API setup
useEffect(() => {
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: (metadata === null || metadata === void 0 ? void 0 : metadata.title) || `Narration: ${title}`,
artist: (metadata === null || metadata === void 0 ? void 0 : metadata.artist) || 'React Edge TTS',
album: metadata === null || metadata === void 0 ? void 0 : metadata.album,
artwork: (metadata === null || metadata === void 0 ? void 0 : metadata.artwork) || [
{
src: 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"%3E%3Cpath fill="%232563eb" d="M12 3v9.28a4.39 4.39 0 0 0-1.5-.28C8.01 12 6 14.01 6 16.5S8.01 21 10.5 21c2.31 0 4.2-1.75 4.45-4H15V6h4V3h-7z"/%3E%3C/svg%3E',
sizes: '96x96',
type: 'image/svg+xml',
},
],
});
navigator.mediaSession.setActionHandler('play', () => { var _a; return (_a = audioRef.current) === null || _a === void 0 ? void 0 : _a.play(); });
navigator.mediaSession.setActionHandler('pause', () => { var _a; return (_a = audioRef.current) === null || _a === void 0 ? void 0 : _a.pause(); });
navigator.mediaSession.setActionHandler('seekto', (details) => {
if (details.seekTime && audioRef.current) {
audioRef.current.currentTime = details.seekTime;
}
});
}
}, [title, metadata]);
// Audio event handlers
const handlePlay = useCallback(() => {
setIsPlaying(true);
onPlay === null || onPlay === void 0 ? void 0 : onPlay();
navigator.mediaSession.playbackState = 'playing';
}, [onPlay]);
const handlePause = useCallback(() => {
setIsPlaying(false);
onPause === null || onPause === void 0 ? void 0 : onPause();
navigator.mediaSession.playbackState = 'paused';
}, [onPause]);
const handleTimeUpdate = useCallback(() => {
if (audioRef.current) {
setCurrentTime(audioRef.current.currentTime);
navigator.mediaSession.setPositionState({
duration: audioRef.current.duration,
playbackRate: audioRef.current.playbackRate,
position: audioRef.current.currentTime,
});
}
}, []);
const handleLoadedMetadata = useCallback(() => {
if (audioRef.current) {
setDuration(audioRef.current.duration);
}
}, []);
const handleVolumeChange = useCallback((e) => {
const newVolume = parseFloat(e.target.value);
setVolume(newVolume);
if (audioRef.current) {
audioRef.current.volume = newVolume;
}
}, []);
const handleSeek = useCallback((e) => {
const time = parseFloat(e.target.value);
if (audioRef.current) {
audioRef.current.currentTime = time;
setCurrentTime(time);
}
}, []);
const togglePlayPause = useCallback(() => {
if (audioRef.current) {
if (isPlaying) {
audioRef.current.pause();
}
else {
audioRef.current.play();
}
}
}, [isPlaying]);
const VolumeIcon = volume === 0 ? VolumeX : volume < 0.5 ? Volume1 : Volume2;
return (React.createElement("div", { className: `react-edge-tts-player ${className}`, style: {
padding: '1rem',
borderRadius: '0.5rem',
backgroundColor: theme.background,
color: theme.text,
...style,
} },
React.createElement("audio", { ref: audioRef, src: audioSrc, onPlay: handlePlay, onPause: handlePause, onEnded: onEnded, onError: (e) => onError === null || onError === void 0 ? void 0 : onError(new Error('Audio playback error')), onTimeUpdate: handleTimeUpdate, onLoadedMetadata: handleLoadedMetadata }),
React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '1rem' } },
React.createElement("button", { onClick: togglePlayPause, style: {
backgroundColor: theme.primary,
color: 'white',
border: 'none',
borderRadius: '50%',
width: '2.5rem',
height: '2.5rem',
cursor: 'pointer',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
padding: '0.5rem',
}, "aria-label": isPlaying ? 'Pause' : 'Play' }, isPlaying ? (React.createElement(Pause, { size: 20, "aria-hidden": "true" })) : (React.createElement(Play, { size: 20, "aria-hidden": "true" }))),
React.createElement("div", { style: { flex: 1 } },
React.createElement("input", { type: "range", min: 0, max: duration, value: currentTime, onChange: handleSeek, style: {
width: '100%',
accentColor: theme.primary,
} }),
showTime && (React.createElement("div", { style: { display: 'flex', justifyContent: 'space-between', fontSize: '0.875rem' } },
React.createElement("span", null, formatTime(currentTime)),
React.createElement("span", null, formatTime(duration))))),
showVolume && (React.createElement("div", { style: { display: 'flex', alignItems: 'center', gap: '0.5rem', minWidth: '100px' } },
React.createElement(VolumeIcon, { size: 20, style: { color: theme.text }, "aria-label": "Volume" }),
React.createElement("input", { type: "range", min: 0, max: 1, step: 0.1, value: volume, onChange: handleVolumeChange, style: { accentColor: theme.primary } }))))));
};
//# sourceMappingURL=NarrationPlayer.js.map