react-native-audio-recorder-player
Version:
React Native Audio Recorder and Player.
365 lines (344 loc) • 11.4 kB
JavaScript
"use strict";
export * from "./AudioRecorderPlayer.nitro.js";
class AudioRecorderPlayerWebImpl {
mediaRecorder = null;
audioContext = null;
audio = null;
recordedChunks = [];
recordingStartTime = 0;
actualRecordedDuration = 0;
recordBackListener = null;
playBackListener = null;
playbackEndListener = null;
recordingInterval = null;
playbackInterval = null;
subscriptionDuration = 10; // Default 10ms for faster updates
currentVolume = 1.0;
recordingUrl = null;
mediaStream = null;
// Recording methods
async startRecorder(_uri, audioSets, meteringEnabled) {
try {
// Get user media
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
channelCount: audioSets?.AudioChannels || 1,
sampleRate: audioSets?.AudioSamplingRate || 44100,
echoCancellation: true,
noiseSuppression: true
}
});
// Setup audio context for metering
if (meteringEnabled && !this.audioContext) {
this.audioContext = new AudioContext();
}
// Create MediaRecorder
const options = {
mimeType: this.getMimeType(audioSets),
audioBitsPerSecond: audioSets?.AudioEncodingBitRate || 128000
};
this.mediaRecorder = new MediaRecorder(stream, options);
this.recordedChunks = [];
this.mediaRecorder.ondataavailable = event => {
if (event.data.size > 0) {
this.recordedChunks.push(event.data);
}
};
// Store stream reference for cleanup
this.mediaStream = stream;
// Start recording with timeslice to get more accurate chunks
this.mediaRecorder.start(100); // Capture data every 100ms
this.recordingStartTime = Date.now();
// Start progress updates
this.startRecordingProgress(meteringEnabled || false);
// Return a placeholder until recording is stopped
return 'recording_in_progress';
} catch (error) {
console.error('Failed to start recording:', error);
throw new Error(`Failed to start recording: ${error}`);
}
}
async pauseRecorder() {
if (this.mediaRecorder && this.mediaRecorder.state === 'recording') {
this.mediaRecorder.pause();
this.stopRecordingProgress();
return 'paused';
}
throw new Error('No active recording to pause');
}
async resumeRecorder() {
if (this.mediaRecorder && this.mediaRecorder.state === 'paused') {
this.mediaRecorder.resume();
this.startRecordingProgress(false);
return 'resumed';
}
throw new Error('No paused recording to resume');
}
async stopRecorder() {
if (this.mediaRecorder) {
return new Promise(resolve => {
const mimeType = this.mediaRecorder.mimeType || 'audio/webm';
// Calculate actual duration before stopping
this.actualRecordedDuration = Date.now() - this.recordingStartTime;
this.mediaRecorder.onstop = () => {
const blob = new Blob(this.recordedChunks, {
type: mimeType
});
const url = URL.createObjectURL(blob);
this.recordingUrl = url;
// Clean up the stream
if (this.mediaStream) {
this.mediaStream.getTracks().forEach(track => track.stop());
this.mediaStream = null;
}
// Clean up media recorder
this.mediaRecorder = null;
resolve(url);
};
this.mediaRecorder.stop();
this.stopRecordingProgress();
});
}
throw new Error('No active recording to stop');
}
// Playback methods
async startPlayer(uri, httpHeaders) {
try {
this.audio = new Audio();
this.audio.volume = this.currentVolume;
if (uri) {
// For remote URLs with headers, we might need to use fetch
if (httpHeaders && Object.keys(httpHeaders).length > 0) {
const response = await fetch(uri, {
headers: httpHeaders
});
const blob = await response.blob();
const url = URL.createObjectURL(blob);
this.audio.src = url;
} else {
this.audio.src = uri;
}
} else if (this.recordingUrl) {
this.audio.src = this.recordingUrl;
} else {
throw new Error('No audio URI provided');
}
// Add ended event listener
this.audio.onended = () => {
this.stopPlaybackProgress();
const finalDuration = this.actualRecordedDuration > 0 ? this.actualRecordedDuration : this.audio.duration * 1000;
if (this.playBackListener) {
// Send final update with exact duration
this.playBackListener({
isMuted: this.audio.muted,
duration: finalDuration,
currentPosition: finalDuration
});
}
if (this.playbackEndListener) {
// Send playback end event
this.playbackEndListener({
duration: finalDuration,
currentPosition: finalDuration
});
}
};
await this.audio.play();
this.startPlaybackProgress();
return uri || this.recordingUrl || '';
} catch (error) {
console.error('Failed to start playback:', error);
throw new Error(`Failed to start playback: ${error}`);
}
}
async stopPlayer() {
if (this.audio) {
this.audio.pause();
this.audio.currentTime = 0;
this.stopPlaybackProgress();
this.audio = null;
return 'stopped';
}
throw new Error('No active playback to stop');
}
async pausePlayer() {
if (this.audio && !this.audio.paused) {
this.audio.pause();
this.stopPlaybackProgress();
return 'paused';
}
throw new Error('No active playback to pause');
}
async resumePlayer() {
if (this.audio && this.audio.paused) {
await this.audio.play();
this.startPlaybackProgress();
return 'resumed';
}
throw new Error('No paused playback to resume');
}
async seekToPlayer(time) {
if (this.audio) {
this.audio.currentTime = time / 1000; // Convert ms to seconds
return `${time}`;
}
throw new Error('No active playback to seek');
}
async setVolume(volume) {
this.currentVolume = Math.max(0, Math.min(1, volume));
if (this.audio) {
this.audio.volume = this.currentVolume;
}
return `${this.currentVolume}`;
}
async setPlaybackSpeed(playbackSpeed) {
if (this.audio) {
this.audio.playbackRate = playbackSpeed;
return `${playbackSpeed}`;
}
throw new Error('No active playback to set speed');
}
// Subscription
setSubscriptionDuration(sec) {
// For web, use milliseconds directly for faster updates
this.subscriptionDuration = Math.max(10, sec * 1000); // Convert to ms, minimum 10ms
}
// Listeners
addRecordBackListener(callback) {
this.recordBackListener = callback;
}
removeRecordBackListener() {
this.recordBackListener = null;
}
addPlayBackListener(callback) {
this.playBackListener = callback;
}
removePlayBackListener() {
this.playBackListener = null;
}
addPlaybackEndListener(callback) {
this.playbackEndListener = callback;
}
removePlaybackEndListener() {
this.playbackEndListener = null;
}
// Utility methods
mmss(secs) {
const minutes = Math.floor(secs / 60);
const seconds = Math.floor(secs % 60);
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
}
mmssss(milisecs) {
const totalSeconds = Math.floor(milisecs / 1000);
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
const milliseconds = Math.floor(milisecs % 1000 / 10);
return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}:${milliseconds.toString().padStart(2, '0')}`;
}
// Private helper methods
getMimeType(_audioSets) {
// Try to use webm/opus for best browser support
if (MediaRecorder.isTypeSupported('audio/webm;codecs=opus')) {
return 'audio/webm;codecs=opus';
}
if (MediaRecorder.isTypeSupported('audio/webm')) {
return 'audio/webm';
}
if (MediaRecorder.isTypeSupported('audio/mp4')) {
return 'audio/mp4';
}
return 'audio/wav';
}
startRecordingProgress(meteringEnabled) {
if (this.recordingInterval) {
clearInterval(this.recordingInterval);
}
const startTime = performance.now();
this.recordingInterval = setInterval(() => {
if (this.recordBackListener && this.mediaRecorder?.state === 'recording') {
const currentPosition = performance.now() - startTime;
const recordSecs = currentPosition / 1000;
this.recordBackListener({
isRecording: true,
currentPosition,
currentMetering: meteringEnabled ? this.getCurrentMetering() : undefined,
recordSecs
});
}
}, this.subscriptionDuration);
}
stopRecordingProgress() {
if (this.recordingInterval) {
clearInterval(this.recordingInterval);
this.recordingInterval = null;
}
}
startPlaybackProgress() {
if (this.playbackInterval) {
clearInterval(this.playbackInterval);
}
this.playbackInterval = setInterval(() => {
if (this.playBackListener && this.audio && !this.audio.paused) {
// Use actual recorded duration if available and audio duration is not accurate
const duration = this.actualRecordedDuration > 0 ? this.actualRecordedDuration : this.audio.duration * 1000;
const currentPosition = this.audio.currentTime * 1000;
// Ensure we don't exceed duration
const safePosition = Math.min(currentPosition, duration);
this.playBackListener({
isMuted: this.audio.muted,
duration: duration,
currentPosition: safePosition
});
// Check if playback has ended
if (this.audio.ended || currentPosition >= duration) {
this.stopPlaybackProgress();
if (this.playBackListener) {
// Send final update with 100% progress
this.playBackListener({
isMuted: this.audio.muted,
duration: duration,
currentPosition: duration
});
}
}
}
}, this.subscriptionDuration);
}
stopPlaybackProgress() {
if (this.playbackInterval) {
clearInterval(this.playbackInterval);
this.playbackInterval = null;
}
}
getCurrentMetering() {
// Simplified metering - in a real implementation, you'd analyze the audio stream
return Math.random() * 40 - 40; // Random value between -40 and 0 dB
}
// Required by HybridObject interface but not used in web
get name() {
return 'AudioRecorderPlayer';
}
equals(other) {
return other === this;
}
get hashCode() {
return 0;
}
toString() {
return 'AudioRecorderPlayer (Web)';
}
dispose() {
this.stopRecorder().catch(() => {});
this.stopPlayer().catch(() => {});
this.removeRecordBackListener();
this.removePlayBackListener();
if (this.audioContext) {
this.audioContext.close();
this.audioContext = null;
}
}
}
// Create singleton instance
const AudioRecorderPlayer = new AudioRecorderPlayerWebImpl();
export default AudioRecorderPlayer;
//# sourceMappingURL=index.web.js.map