diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5fe435..3321fa4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,5 +1,8 @@ name: CI +permissions: + contents: read + on: push: branches: [ main, develop ] @@ -9,8 +12,10 @@ on: jobs: test: runs-on: ubuntu-latest + timeout-minutes: 20 strategy: + fail-fast: true matrix: node-version: [16.x, 18.x, 20.x] @@ -26,12 +31,20 @@ jobs: - name: Install dependencies run: npm ci + + - name: Lint (Node 20 quality gate) + if: matrix.node-version == '20.x' + run: npm run lint - name: Build project run: npm run build + + - name: Demo route smoke test (Node 20) + if: matrix.node-version == '20.x' + run: npm run test:demo-smoke - name: Run tests - run: npm test + run: npm test -- --runInBand - name: Upload coverage reports if: matrix.node-version == '20.x' @@ -39,10 +52,12 @@ jobs: with: name: coverage-reports path: coverage/ + if-no-files-found: ignore build: runs-on: ubuntu-latest needs: test + timeout-minutes: 15 steps: - name: Checkout code @@ -56,12 +71,18 @@ jobs: - name: Install dependencies run: npm ci + + - name: Lint quality gate + run: npm run lint - name: Build all targets run: npm run build + + - name: Validate package exports + run: npm run verify:exports - name: Upload build artifacts uses: actions/upload-artifact@v4 with: name: build-artifacts - path: dist/ \ No newline at end of file + path: dist/ diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 0bd89a2..eef67b8 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -1,5 +1,8 @@ name: PR Validation +permissions: + contents: read + on: pull_request: branches: [ main, develop ] @@ -7,83 +10,111 @@ on: jobs: validate: runs-on: ubuntu-latest - + timeout-minutes: 20 + steps: - name: Checkout code uses: actions/checkout@v4 - + with: + fetch-depth: 0 + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20.x' cache: 'npm' - + - name: Install dependencies run: npm ci - + - name: Check for package.json changes id: package-check run: | - if git diff --name-only origin/main...HEAD | grep -q "package.json"; then + set -euo pipefail + if git diff --name-only "${{ github.event.pull_request.base.sha }}" "${{ github.sha }}" | grep -q '^package.json$'; then echo "package_changed=true" >> $GITHUB_OUTPUT else echo "package_changed=false" >> $GITHUB_OUTPUT fi - + - name: Validate version bump if: steps.package-check.outputs.package_changed == 'true' run: | + set -euo pipefail CURRENT_VERSION=$(node -p "require('./package.json').version") - git checkout origin/main -- package.json - MAIN_VERSION=$(node -p "require('./package.json').version") - git checkout HEAD -- package.json - + MAIN_VERSION=$(git show "${{ github.event.pull_request.base.sha }}:package.json" | node -e "let d='';process.stdin.on('data',c=>d+=c);process.stdin.on('end',()=>console.log(JSON.parse(d).version));") + echo "Main branch version: $MAIN_VERSION" echo "PR version: $CURRENT_VERSION" - + if [ "$CURRENT_VERSION" = "$MAIN_VERSION" ]; then - echo "โ Version not bumped in package.json" + echo "Version not bumped in package.json" exit 1 else - echo "โ Version properly bumped: $MAIN_VERSION โ $CURRENT_VERSION" + echo "Version properly bumped: $MAIN_VERSION -> $CURRENT_VERSION" fi - + + - name: Lint quality gate + run: npm run lint + - name: Build project run: npm run build - + + - name: Demo route smoke test + run: npm run test:demo-smoke + - name: Run tests - run: npm test - + run: npm test -- --runInBand + - name: Check package size run: | - npm pack - PACKAGE_SIZE=$(du -sh *.tgz | cut -f1) - echo "๐ฆ Package size: $PACKAGE_SIZE" - + set -euo pipefail + PACKAGE_FILE=$(npm pack --silent) + PACKAGE_SIZE_BYTES=$(wc -c < "$PACKAGE_FILE") + echo "Package size (bytes): $PACKAGE_SIZE_BYTES" + rm -f "$PACKAGE_FILE" + - name: Validate exports run: | echo "Validating package exports..." node -e " - const pkg = require('./package.json'); const fs = require('fs'); - + // Check main exports exist console.log('Checking main exports...'); if (!fs.existsSync('./dist/index.cjs')) throw new Error('Missing main CJS build'); if (!fs.existsSync('./dist/index.esm.js')) throw new Error('Missing main ESM build'); if (!fs.existsSync('./dist/index.d.ts')) throw new Error('Missing main types'); - + // Check browser exports exist console.log('Checking browser exports...'); if (!fs.existsSync('./dist/browser.esm.js')) throw new Error('Missing browser ESM build'); if (!fs.existsSync('./dist/browser.cjs')) throw new Error('Missing browser CJS build'); if (!fs.existsSync('./dist/browser.d.ts')) throw new Error('Missing browser types'); - + // Check server exports exist console.log('Checking server exports...'); if (!fs.existsSync('./dist/server.esm.js')) throw new Error('Missing server ESM build'); if (!fs.existsSync('./dist/server.cjs')) throw new Error('Missing server CJS build'); if (!fs.existsSync('./dist/server.d.ts')) throw new Error('Missing server types'); - - console.log('โ All exports validated successfully'); - " \ No newline at end of file + + // Check react exports exist + console.log('Checking react exports...'); + if (!fs.existsSync('./dist/react.esm.js')) throw new Error('Missing react ESM build'); + if (!fs.existsSync('./dist/react.cjs')) throw new Error('Missing react CJS build'); + if (!fs.existsSync('./dist/react.d.ts')) throw new Error('Missing react types'); + + // Check vue exports exist + console.log('Checking vue exports...'); + if (!fs.existsSync('./dist/vue.esm.js')) throw new Error('Missing vue ESM build'); + if (!fs.existsSync('./dist/vue.cjs')) throw new Error('Missing vue CJS build'); + if (!fs.existsSync('./dist/vue.d.ts')) throw new Error('Missing vue types'); + + console.log('All exports validated successfully'); + " + + - name: Validate runtime subpath exports + run: | + node scripts/verify-exports.js + node scripts/verify-hook-export.js + node scripts/verify-vue-export.js diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 92d22d9..bbda749 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -1,5 +1,8 @@ name: Publish to NPM +permissions: + contents: write + on: push: tags: @@ -14,10 +17,13 @@ on: jobs: publish: runs-on: ubuntu-latest + timeout-minutes: 20 steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Setup Node.js uses: actions/setup-node@v4 @@ -28,17 +34,24 @@ jobs: - name: Install dependencies run: npm ci + + - name: Lint quality gate + run: npm run lint - name: Run tests - run: npm test + run: npm test -- --runInBand - name: Build project run: npm run build + + - name: Validate package exports + run: npm run verify:exports - name: Verify package contents run: npm pack --dry-run - name: Publish to NPM + if: ${{ secrets.NPM_TOKEN != '' }} run: npm publish env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} @@ -51,4 +64,4 @@ jobs: tag_name: ${{ github.ref }} release_name: Release ${{ github.ref }} draft: false - prerelease: false \ No newline at end of file + prerelease: false diff --git a/.gitignore b/.gitignore index e81d430..793a5e6 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,10 @@ discord-engagement-plan.md github-responses-ready-to-post.md phase1-response-templates.md CLAUDE.md + +# Local maintenance artifacts +code-review-report.md +todo.md + +# Exported patch diffs (generated artifacts) +patches/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 096a385..80d7914 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [1.1.7] - 2026-02-06 + +### Added +- Comprehensive video streaming support (MP4, WebM, M3U8/HLS adaptive streaming) +- Video examples: `examples/video-streaming.js` with 6 complete examples +- React video player component: `examples/react-video-player.tsx` with basic, advanced, and playlist modes +- Video feature documentation in README with code examples and usage patterns + +### Changed +- Expanded README with explicit video streaming feature documentation +- Updated package description to include video media support +- Enhanced package keywords to cover video formats (hls-streaming, m3u8-playlist, mp4-streaming, video-streaming) +- Improved gitignore to exclude generated maintenance artifacts and patch diffs + +### Documentation +- Clarified universal media streaming capabilities (audio and video) +- Added dedicated "Video Streaming" section with MP4, M3U8/HLS, and seeking examples +- Updated problem/solution sections to address video codec and HLS challenges +- Documented range request support for video seeking + ## [1.1.6] - 2025-10-21 ### Fixed diff --git a/README.md b/README.md index 3bf22bd..d172a6c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@
- Bypasses CORS & WebKit codec issues in Tauri and Electron audio apps with ease. + Bypasses CORS & WebKit codec issues in Tauri and Electron apps with universal audio and video streaming support.
@@ -39,23 +39,28 @@
## Features
-- **CORS Bypass** - Play external audio URLs without CORS restrictions
+- **CORS Bypass** - Play external audio/video URLs without CORS restrictions
+- **Universal Media Streaming** - Supports audio (MP3, AAC, OGG, WAV) and video (MP4, M3U8/HLS, WebM)
- **Auto-Start Proxy** - Automatically spin up proxy server when needed (Node.js only)
- **Tauri v1 & v2 Support** - Works seamlessly with both Tauri versions
- **Debug Logger** - Multi-level logging with category filtering for troubleshooting
- **Telemetry System** - Optional performance monitoring and event tracking
- **React/Vue Integration** - Ready-to-use hooks and composables for seamless framework integration
- **Enhanced Codec Detection** - Comprehensive format testing with real-time capabilities mapping
-- **Audio Metadata Extraction** - Get duration, format, bitrate from audio files
+- **Media Metadata Extraction** - Get duration, format, bitrate from audio/video files
- **Audio Device Enumeration** - List and manage available audio devices
- **WebKit Compatibility** - Solves codec issues in Tauri/Electron WebView
- **Environment Detection** - Automatically detects Tauri, Electron, or web environment
- **Smart Fallbacks** - Graceful degradation when proxy unavailable
+- **URL Security Guardrails** - Protocol allowlist and private-network blocking for proxy targets
- **Actionable Error Messages** - Clear error messages with specific steps to fix issues
- **Retry Logic** - Configurable retry attempts with delays
- **Health Monitoring** - Built-in health and info endpoints
+- **Stream Lifecycle Hardening** - Conservative cleanup for aborted/closed proxy streams
+- **Deterministic Core Tests** - Local upstream mock coverage for info/proxy/timeout paths
- **TypeScript** - Full type safety and IntelliSense support with JSDoc comments
-- **Range Requests** - Support for audio seeking and streaming
+- **Range Requests** - Full support for seeking in audio and video streams
+- **HLS/Adaptive Streaming** - Handle M3U8 playlists and adaptive bitrate streaming
- **Tree-Shakeable** - Optimized for smaller bundles with dead code elimination
- **Interactive Demo** - Live testing environment with auto-detection
@@ -106,20 +111,22 @@ Seamless proxy server integration with automatic port detection:
When building desktop applications with web technologies (Tauri, Electron), developers often face:
-1. **CORS Issues**: External audio URLs (podcasts, radio streams) are blocked due to Cross-Origin Resource Sharing policies
-2. **Codec Compatibility**: WebKit may not support certain audio codecs even with proper GStreamer plugins installed
-3. **Authentication**: Some audio streams require special headers or authentication
-4. **Redirects**: Many podcast/streaming URLs use multiple redirects that cause issues
+1. **CORS Issues**: External audio/video URLs (podcasts, radio streams, video content) are blocked due to Cross-Origin Resource Sharing policies
+2. **Codec Compatibility**: WebKit may not support certain audio/video codecs even with proper GStreamer plugins installed
+3. **Authentication**: Some media streams require special headers or authentication
+4. **Redirects**: Many podcast/streaming/video URLs use multiple redirects that cause issues
+5. **HLS/Adaptive Streaming**: M3U8 playlists and adaptive bitrate video may fail to load properly
## The Solution
Desktop Audio Proxy provides:
-- **Automatic CORS bypass** for external audio URLs
-- **Codec transcoding** support (optional, requires ffmpeg)
+- **Automatic CORS bypass** for external audio and video URLs
+- **Universal media streaming** - works with audio (MP3, AAC, WAV) and video (MP4, WebM, M3U8/HLS)
+- **Range request support** - enables seeking in both audio and video
- **Smart redirect handling** with configurable limits
- **Automatic environment detection** (Tauri/Electron/Web)
-- **Optional caching** for better performance
+- **Optional info endpoint caching** for faster repeated metadata checks
- **Simple API** that works like a drop-in replacement
## Installation
@@ -132,6 +139,13 @@ yarn add desktop-audio-proxy
pnpm add desktop-audio-proxy
```
+### Runtime Requirements
+
+- Node.js `>=14` for package compatibility (per `engines`)
+- Node.js `>=18` recommended for built-in `fetch` support
+- If you run in Node.js 14/16, provide a global `fetch` polyfill (for client health/info checks)
+- React/Vue entry points require their respective framework in your app (`react` or `vue`)
+
## Package Exports
This library provides multiple entry points optimized for different environments:
@@ -157,10 +171,21 @@ import { startProxyServer, AudioProxyServer } from 'desktop-audio-proxy/server';
**See Desktop Audio Proxy in action!** Our comprehensive demo provides real-time testing with automatic library detection:
```bash
-npm run demo # Start demo with auto-server
-npm run demo:serve # Serve demo on http://localhost:8080
+npm run demo # Build React demo bundle + start full demo server on http://localhost:8080
+npm run build:react-demo # Rebuild React demo bundle only
+npm run demo:serve # Start static demo server on http://localhost:8080
```
+`demo` and `demo:serve` both preserve the legacy example route:
+`/examples/react-video-player.tsx` -> `/examples/react-example.tsx`
+
+**Demo Route Map:**
+- `/` - Main interactive demo UI
+- `/react-player.html` - React demo player page
+- `/telemetry-dashboard.html` - Telemetry event viewer
+- `/examples/react-example.tsx` - Current React example source
+- `/examples/react-video-player.tsx` - Legacy path (302 redirect to `react-example.tsx`)
+
**Demo Features:**
- **Auto-Detection** - Automatically finds and loads available library builds (local, CDN, various formats)
- **Version Detection** - Shows current library version with upgrade recommendations
@@ -256,6 +281,109 @@ const audioClient = createAudioClient({
const playableUrl = await audioClient.getPlayableUrl('https://example.com/audio.mp3');
```
+### Proxy Endpoint Reference
+
+When the proxy server is running (for example at `http://localhost:3002`):
+
+- `GET /health` - Liveness/config snapshot used by client auto-detection.
+- `GET /info?url= Eliminates CORS issues for audio streaming in desktop apps
+ Library Version: ${safeVersion}
Enhanced Features: ${versionInfo.isEnhanced ? 'Available' : 'Not Available'}
- ${versionInfo.features.length > 0 ?
- `Active Features: ${versionInfo.features.join(', ')}
` : ''
+ ${safeFeatures.length > 0 ?
+ `Active Features: ${safeFeatures.join(', ')}
` : ''
}
- ${versionInfo.multipleVersionsWarning ?
- `
- Original URL: ${url}
+ Original URL: ${safeUrl}
Result: Server has CORS headers or allows cross-origin access
Note: This URL might not need the proxy, but most audio URLs do
`;
@@ -704,23 +724,28 @@ class DesktopAudioProxyDemo {
let errorType = 'Network/CORS Error';
let errorExplanation = 'Server lacks CORS headers - browser blocks cross-origin access';
- if (error.message.includes('CORS') || error.message.includes('cors')) {
+ const errorMessage = getErrorMessage(error);
+ if (errorMessage.includes('CORS') || errorMessage.includes('cors')) {
errorType = 'CORS Policy Violation';
errorExplanation = 'Server does not allow cross-origin requests from browsers';
- } else if (error.message.includes('timeout') || error.message.includes('abort')) {
+ } else if (errorMessage.includes('timeout') || errorMessage.includes('abort')) {
errorType = 'Request Timeout/Blocked';
errorExplanation = 'Server may be blocking cross-origin requests or responding slowly';
- } else if (error.message.includes('403') || error.message.includes('Forbidden')) {
+ } else if (errorMessage.includes('403') || errorMessage.includes('Forbidden')) {
errorType = 'CORS Preflight Failed';
errorExplanation = 'Server rejected the CORS preflight request (OPTIONS method)';
}
errorElement.textContent = `${errorType} - This demonstrates why the proxy is needed!`;
+ const safeErrorType = escapeHtml(errorType);
+ const safeErrorMessage = escapeHtml(errorMessage);
+ const safeUrl = escapeHtml(url);
+ const safeErrorExplanation = escapeHtml(errorExplanation);
detailsElement.innerHTML = `
- โ CORS Blocked (Expected): ${errorType}
- Error Details: ${error.message}
- Original URL: ${url}
- Why this failed: ${errorExplanation}
+ โ CORS Blocked (Expected): ${safeErrorType}
+ Error Details: ${safeErrorMessage}
+ Original URL: ${safeUrl}
+ Why this failed: ${safeErrorExplanation}
Solution: Use the proxy test below to see CORS bypass in action
This demonstrates: Why Desktop Audio Proxy is needed for most audio URLs
`;
@@ -789,16 +814,21 @@ class DesktopAudioProxyDemo {
resultContainer.classList.add('success');
successElement.textContent = 'CORS bypass successful via proxy server!';
+ const safePlayableUrl = escapeHtml(playableUrl);
+ const safeEnvironment = escapeHtml(this.environment ?? 'unknown');
+ const safeWorkingProxyUrl = escapeHtml(this.workingProxyUrl ?? 'unknown');
+ const safeRetryAttempts = escapeHtml(this.audioClient.options.retryAttempts);
+ const safeFallback = escapeHtml(this.audioClient.options.fallbackToOriginal);
detailsElement.innerHTML = `
Library Method Used: audioClient.getPlayableUrl()
Proxied URL:
- ${playableUrl}
+ ${safePlayableUrl}
Process: Original URL โ Proxy Server โ Browser (CORS bypassed)
Library Features Active:
- โข Environment Detection: ${this.environment}
- โข Proxy Server: ${this.workingProxyUrl}
- โข Auto Retry: ${this.audioClient.options.retryAttempts} attempts
- โข Fallback Enabled: ${this.audioClient.options.fallbackToOriginal}
+ โข Environment Detection: ${safeEnvironment}
+ โข Proxy Server: ${safeWorkingProxyUrl}
+ โข Auto Retry: ${safeRetryAttempts} attempts
+ โข Fallback Enabled: ${safeFallback}
`;
// Set the working proxied URL
@@ -814,7 +844,8 @@ class DesktopAudioProxyDemo {
resultContainer.classList.add('error');
// Check if it's a non-audio content type issue
- const isNonAudioContent = error.message.includes('text/html') ||
+ const errorMessage = getErrorMessage(error);
+ const isNonAudioContent = errorMessage.includes('text/html') ||
url.includes('youtube.com') ||
url.includes('spotify.com') ||
url.includes('soundcloud.com');
@@ -829,22 +860,26 @@ class DesktopAudioProxyDemo {
Solution: Use direct audio file URLs only
`;
} else if (!this.proxyServerAvailable) {
+ const safeErrorMessage = escapeHtml(errorMessage);
+ const safePort = escapeHtml(this.workingProxyUrl?.split(':')[2] || '3002');
detailsElement.innerHTML = `
- Error: ${error.message}
+ Error: ${safeErrorMessage}
Issue: Proxy server is not available
Solution:
โข Start proxy server: npm run proxy:start
- โข Check that port ${this.workingProxyUrl?.split(':')[2] || '3002'} is not blocked
+ โข Check that port ${safePort} is not blocked
โข Restart the demo
`;
} else {
+ const safeErrorMessage = escapeHtml(errorMessage);
+ const safeRetryAttempts = escapeHtml(this.audioClient.options.retryAttempts);
detailsElement.innerHTML = `
- Error: ${error.message}
+ Error: ${safeErrorMessage}
Possible Issues:
โข URL might be inaccessible or return 404
โข Server might not allow proxying
โข Audio format might not be supported
- Library attempted: ${this.audioClient.options.retryAttempts} retries with proxy
+ Library attempted: ${safeRetryAttempts} retries with proxy
`;
}
}
@@ -864,10 +899,12 @@ class DesktopAudioProxyDemo {
showError(message, error = null) {
const statusPanel = document.querySelector('.status-panel');
+ const safeMessage = escapeHtml(message);
+ const safeErrorMessage = error ? escapeHtml(getErrorMessage(error)) : '';
statusPanel.innerHTML = `
โ Error
-
- ${error ? `Desktop Audio Proxy
โ What It CANNOT Do:
Implementation Code
@@ -271,4 +277,4 @@ import { ElectronAudioService } from 'desktop-audio-proxy';
const audioService = new ElectronAudioService({
- enableTranscoding: true
+ audioOptions: {
+ proxyServerConfig: {
+ enableLogging: true
+ }
+ }
});
const streamUrl = await audioService.getStreamableUrl(originalUrl);Implementation Code
window.copyCode = copyCode;