z-web-audio-stream
Version:
iOS Safari-safe Web Audio streaming with separated download/storage optimization, instant playback, and memory management
500 lines (388 loc) • 16.6 kB
Markdown
# web-audio-stream
iOS Safari-safe Web Audio streaming with **separated download/storage optimization**, instant playback and memory management.
## ✨ Features
- **🚀 Instant Playback**: Start playing within 100-500ms using separated download strategy
- **📡 Download Optimization**: Network-optimized chunks (64KB-512KB) separate from storage chunks (1-3MB)
- **🔄 Streaming Assembly**: Real-time chunk assembly for seamless playback transitions
- **🍎 iOS Safari Compatibility**: Fixes pitch/speed issues and prevents page reloads
- **⚡ Progressive Loading**: Background streaming with seamless buffer replacement
- **🎯 Range Request Support**: Parallel downloads with configurable concurrency
- **💾 Smart Caching**: IndexedDB storage with automatic cleanup
- **🔊 AudioWorklet**: High-performance audio processing
- **📱 Memory Safe**: Adaptive chunk sizing to prevent iOS crashes
- **📊 Performance Monitoring**: Real-time metrics and adaptive optimization
- **🎵 Audio Management**: Full audio state tracking with duration, cache, and memory info
- **🔧 Easy Setup**: Simple API with TypeScript support
## 🚨 iOS Safari Issues This Fixes
1. **Sample Rate Mismatches**: Causes high-pitched/fast audio playback
2. **Memory Pressure**: Large audio files cause page reloads on iOS
3. **IndexedDB Failures**: Safari iOS fails IndexedDB operations randomly
4. **AudioContext Bugs**: Broken state detection and recovery
## 📦 Installation
```bash
npm install z-web-audio-stream
# or
pnpm add z-web-audio-stream
# or
yarn add z-web-audio-stream
```
## 🚀 Quick Start
### 1. Copy the AudioWorklet file
First, copy the AudioWorklet processor to your public directory:
```bash
# Using CLI (recommended)
npx z-web-audio-stream-cli deploy
# Or manually copy from node_modules
cp node_modules/z-web-audio-stream/dist/audio-worklet-processor.js public/
```
### 2. Instant Playback with Separated Optimization (Recommended)
```typescript
import { setupInstantAudio } from 'z-web-audio-stream';
// Initialize with separated download/storage optimization
const manager = await setupInstantAudio({
downloadChunkSize: 256 * 1024, // 256KB downloads for network optimization
storageChunkSize: 2 * 1024 * 1024, // 2MB chunks for IndexedDB efficiency
playbackChunkSize: 384 * 1024, // 384KB for instant playback start
enablePerformanceLogging: true, // See detailed performance metrics
onTimeUpdate: (currentTime, duration) => {
console.log(`Playing: ${currentTime}s / ${duration}s`);
},
onProgressiveLoadingStatus: (status, data) => {
if (status === 'STARTED') {
console.log('🚀 Separated instant playback started!');
console.log(`Strategy: ${data.strategy}`);
}
}
});
// Start playing instantly - audio begins within 500ms
await manager.playInstantly('/audio/song.mp3', 'song-1', 'My Song');
```
## 🏗️ Separated Download/Storage Architecture
WebAudioStream v1.2.0+ uses a sophisticated **three-layer architecture** that separates concerns for optimal performance:
### 📡 Layer 1: Download Manager
- **Purpose**: Network transfer optimization
- **Chunk Size**: 64KB-512KB (optimized for HTTP/2 and mobile networks)
- **Features**: Parallel downloads, range requests, connection speed adaptation
- **Benefits**: Faster initial response, better network utilization
### 🔄 Layer 2: Streaming Assembler
- **Purpose**: Real-time chunk assembly and playback preparation
- **Chunk Size**: 256KB-384KB for first playback chunk, larger for storage
- **Features**: Streaming assembly, immediate playback readiness detection
- **Benefits**: Sub-500ms playback start, seamless transitions
### 💾 Layer 3: Storage Manager
- **Purpose**: IndexedDB optimization and memory management
- **Chunk Size**: 1-3MB (optimized for browser storage efficiency)
- **Features**: iOS Safari retry logic, automatic cleanup, obfuscation
- **Benefits**: Reliable caching, memory safety, privacy protection
```
Download (256KB) → Assembly (384KB) → Storage (2MB) → Playback
↓ ↓ ↓ ↓
Fast Network Instant Start Efficient Cache Smooth Audio
```
### 3. Basic Usage
```typescript
import { setupWebAudio } from 'z-web-audio-stream';
// Initialize with iOS-safe defaults
const manager = await setupWebAudio({
enableInstantPlayback: true, // Enable instant playback
onTimeUpdate: (currentTime, duration) => {
console.log(`Playing: ${currentTime}s / ${duration}s`);
},
onEnded: () => {
console.log('Playback finished');
},
onError: (error) => {
console.error('Playback error:', error);
}
});
// Traditional loading (waits for full download)
await manager.loadAndPlay('/audio/song.mp3', 'song-1', 'My Song');
// OR use instant playback (recommended)
await manager.playInstantly('/audio/song.mp3', 'song-1', 'My Song');
// Control playback
await manager.pause();
await manager.resume();
await manager.seek(30); // Seek to 30 seconds
manager.setVolume(0.8); // 80% volume
```
### 4. Audio Management (v1.3.0+)
```typescript
import { setupInstantAudio } from 'z-web-audio-stream';
const manager = await setupInstantAudio();
// Load and play some tracks
await manager.playInstantly('/audio/song1.mp3', 'song-1', 'First Song');
await manager.playInstantly('/audio/song2.mp3', 'song-2', 'Second Song');
// Check audio state and get metadata
const isLoaded = await manager.isAudioLoaded('song-1');
console.log('Song 1 loaded:', isLoaded); // true
const duration = manager.getBufferDuration('song-1');
console.log('Song 1 duration:', duration); // e.g., 240.5 seconds
// Get all cached tracks with metadata
const cachedTracks = await manager.getCachedTracks();
cachedTracks.forEach(track => {
console.log(`Track: ${track.name}`);
console.log(`Duration: ${track.duration}s`);
console.log(`Size: ${(track.size / 1024 / 1024).toFixed(1)}MB`);
console.log(`Loaded in memory: ${track.isLoaded}`);
console.log(`Last accessed: ${track.lastAccessed.toLocaleString()}`);
});
// Perfect for building audio players with:
// - Track duration display
// - Cache management
// - Memory usage optimization
// - Playlist state tracking
```
### 5. Privacy & Custom Storage Keys (v1.4.0+)
```typescript
import { setupInstantAudio, setupWebAudio } from 'z-web-audio-stream';
// Use your own obfuscation key for privacy
const manager = await setupInstantAudio({
obfuscationKey: 'my-app-secret-key-2024'
});
// Or with basic setup
const basicManager = await setupWebAudio({
obfuscationKey: 'custom-privacy-key'
});
// Benefits:
// - Your app's cached audio data is obfuscated with your key
// - Prevents other apps from easily reading your cached data
// - Each app/environment can use different keys for data isolation
// - Backward compatible - existing data works with default key
```
### 6. Advanced Configuration
```typescript
import { WebAudioManager, AudioChunkStore } from 'z-web-audio-stream';
const manager = new WebAudioManager({
workletPath: '/audio-worklet-processor.js',
enableCache: true,
enableInstantPlayback: true,
instantPlaybackConfig: {
initialChunkSize: 384 * 1024, // 384KB for instant start
subsequentChunkSize: 2 * 1024 * 1024, // 2MB for streaming
maxInitialWaitTime: 500, // 500ms max wait
strategy: 'auto' // auto, always, never
},
maxCacheSize: 1024 * 1024 * 1024, // 1GB
onProgressiveLoadingStatus: (status, data) => {
console.log('Status:', status, data);
}
});
await manager.initialize();
// Get performance recommendations
const strategy = manager.getPlaybackStrategy('/audio/song.mp3', {
estimatedFileSize: 5 * 1024 * 1024,
connectionSpeed: 'medium',
deviceType: 'mobile'
});
console.log('Recommended strategy:', strategy);
// Use instant playback with progress tracking
await manager.playInstantly('/audio/song.mp3', 'song-1', 'My Song', {
onChunkLoaded: (loaded, total) => {
console.log(`Chunks loaded: ${loaded}/${total}`);
},
onFullyLoaded: () => {
console.log('Full audio loaded in background');
}
});
// Preload for smooth transitions
await manager.preloadAudio('/audio/next-song.mp3', 'song-2', 'Next Song');
// Get performance metrics
const metrics = manager.getInstantPlaybackMetrics();
console.log('Performance metrics:', metrics);
```
## 🚀 Instant Playback
### How It Works
1. **Smart Chunking**: Loads small initial chunk (256-384KB) for instant start
2. **Background Streaming**: Continues loading larger chunks (1-2MB) in background
3. **Seamless Replacement**: Replaces buffer without interrupting playback
4. **Range Request Support**: Uses HTTP Range requests when server supports them
5. **Adaptive Strategy**: Automatically chooses best approach based on conditions
### Performance Targets
- **Start Time**: < 500ms on 3G, < 200ms on WiFi
- **Buffer Switch**: < 50ms seamless transitions
- **Memory Usage**: Optimized for iOS Safari limits
- **Error Recovery**: Graceful fallback to standard loading
### Configuration Options
```typescript
const config: InstantPlaybackConfig = {
initialChunkSize: 384 * 1024, // First chunk size (384KB)
subsequentChunkSize: 2 * 1024 * 1024, // Streaming chunks (2MB)
predictiveLoadingThreshold: 0.75, // Start next chunk at 75%
maxInitialWaitTime: 500, // Max wait for first chunk
strategy: 'auto' // auto | always | never
};
manager.enableInstantMode(config);
```
## 🍎 iOS Safari Optimizations
### Automatic Features
- **Sample Rate Monitoring**: Detects and fixes iOS sample rate bugs
- **Memory-Safe Chunks**: 256KB-1MB chunks on iOS vs 2-8MB on desktop
- **IndexedDB Retry Logic**: 3-attempt retry with delays for Safari
- **Broken State Detection**: Plays dummy buffer to reset AudioContext
- **Instant Playback Limits**: Smaller chunks to prevent iOS memory pressure
### iOS-Specific Behavior
```typescript
import { isIOSSafari } from 'z-web-audio-stream';
if (isIOSSafari()) {
console.log('iOS Safari detected - optimizations active');
// All optimizations are automatic:
// - Smaller chunk sizes
// - Retry logic
// - Sample rate monitoring
// - Memory pressure handling
}
```
## 📋 API Reference
### WebAudioManager
The main class for audio management.
```typescript
class WebAudioManager {
constructor(options?: WebAudioManagerOptions)
// Core methods
async initialize(): Promise<void>
async loadAudio(url: string, trackId: string, progressCallback?: Function): Promise<AudioBuffer>
async loadAndPlay(url: string, trackId: string, name?: string): Promise<void>
async preloadAudio(url: string, trackId: string, name?: string): Promise<void>
// Instant playback methods
async playInstantly(url: string, trackId: string, name: string, options?: {
forceInstant?: boolean;
onChunkLoaded?: (chunkIndex: number, totalChunks: number) => void;
onFullyLoaded?: () => void;
}): Promise<void>
getPlaybackStrategy(url: string, options?: {
estimatedFileSize?: number;
connectionSpeed?: 'slow' | 'medium' | 'fast';
deviceType?: 'mobile' | 'desktop';
}): { strategy: 'instant' | 'progressive' | 'standard'; reasoning: string; }
enableInstantMode(config?: Partial<InstantPlaybackConfig>): void
disableInstantMode(): void
getInstantPlaybackMetrics(): InstantPlaybackMetrics
// Playback control
async play(trackId: string): Promise<void>
async pause(): Promise<void>
async resume(): Promise<void>
async seek(time: number): Promise<void>
setVolume(volume: number): void
getCurrentTime(): number
// Audio management methods (v1.3.0+)
getBufferDuration(trackId: string): number | null
async isAudioLoaded(trackId: string): Promise<boolean>
async getCachedTracks(): Promise<Array<{
trackId: string;
name?: string;
duration?: number;
size: number;
lastAccessed: Date;
isLoaded: boolean;
}>>
// Cleanup
async cleanup(): Promise<void>
}
```
### AudioChunkStore
IndexedDB-based storage with iOS Safari compatibility.
```typescript
class AudioChunkStore {
constructor(audioContext: AudioContext, instantConfig?: Partial<InstantChunkConfig>)
async initialize(): Promise<void>
async storeAudio(url: string, trackId: string, name: string, progressCallback?: ProgressCallback): Promise<AudioMetadata>
async storeAudioStreaming(url: string, trackId: string, name: string, options?: {
initialChunkSize?: number;
subsequentChunkSize?: number;
useRangeRequests?: boolean;
progressCallback?: ProgressCallback;
}): Promise<AudioMetadata>
async getAudioBuffer(trackId: string, startChunk?: number, chunkCount?: number): Promise<AudioBuffer | null>
async getFirstChunk(trackId: string): Promise<AudioBuffer | null>
async getProgressiveChunks(trackId: string, startChunk?: number, maxChunks?: number): Promise<AudioBuffer | null>
async isStored(trackId: string): Promise<boolean>
async removeTrack(trackId: string): Promise<void>
async cleanup(): Promise<void>
configureInstantMode(config: Partial<InstantChunkConfig>): void
getInstantPlaybackMetrics(): InstantPlaybackMetrics
async getStorageInfo(): Promise<StorageInfo>
}
```
## 🔧 Framework Integrations
### Astro
Install the Astro integration:
```bash
npm install z-astro-web-audio-stream
```
```javascript
// astro.config.mjs
import { defineConfig } from 'astro/config';
import webAudioStream from 'z-astro-web-audio-stream';
export default defineConfig({
integrations: [
webAudioStream({
// Automatically copies worklet file to public/
workletPath: '/audio-worklet-processor.js'
})
]
});
```
### React/Vue/Svelte
```typescript
// hooks/useWebAudio.ts
import { useEffect, useState } from 'react';
import { setupWebAudio, WebAudioManager } from 'z-web-audio-stream';
export function useWebAudio() {
const [manager, setManager] = useState<WebAudioManager | null>(null);
const [isReady, setIsReady] = useState(false);
useEffect(() => {
setupInstantAudio({
onTimeUpdate: (currentTime, duration) => {
// Handle time updates
},
onEnded: () => {
// Handle playback end
}
}).then(audioManager => {
setManager(audioManager);
setIsReady(true);
});
return () => {
manager?.cleanup();
};
}, []);
return { manager, isReady };
}
```
## 🐛 Troubleshooting
### Common Issues
**Audio plays too fast/high-pitched on iOS**
- ✅ Fixed automatically by sample rate monitoring and iOS-safe AudioContext
**Page reloads when loading large audio files on iOS**
- ✅ Fixed by adaptive chunk sizing (1-2MB max on iOS)
**IndexedDB errors on Safari**
- ✅ Fixed by retry logic with exponential backoff
**No audio on first interaction**
- Ensure you're calling `initialize()` after user gesture (click/touch)
### Debug Mode
```typescript
// Enable verbose logging for iOS issues
const manager = new WebAudioManager({
// iOS debugging will automatically log sample rates, chunk sizes, etc.
});
```
## 📊 Performance
### Memory Usage
- **Desktop**: 3-8MB chunks, up to 1GB cache
- **iOS Safari**: 1-2MB chunks, intelligent cleanup
- **Automatic**: Cache cleanup based on age and storage limits
### Network Optimization
- Progressive loading starts playback with first chunk
- Adaptive chunk sizing based on connection speed
- Preloading for seamless track transitions
## 🤝 Contributing
Issues and PRs welcome! This package specifically targets iOS Safari audio bugs.
### Common iOS Safari Issues We Fix
1. **Sample Rate Bug**: AudioContext.sampleRate changes unexpectedly
2. **Memory Pressure**: Large audio files cause page reloads
3. **IndexedDB Reliability**: Random failures on first connection
4. **Broken AudioContext**: Requires dummy buffer to reset state
## 📄 License
MIT License - feel free to use in your projects!
## 🙏 Credits
Built by the StreamFi team to solve iOS Safari audio streaming issues. Based on research into iOS Web Audio API bugs and memory management.