audio.libx.js
Version:
Comprehensive audio library with progressive streaming, recording capabilities, real-time processing, and intelligent caching for web applications
256 lines • 9.2 kB
JavaScript
import { MediaSourceError } from './types.js';
export class MediaSourceHelper {
constructor() {
this._supportedMimeTypes = [];
this._isSupported = false;
this._hasManagedMediaSource = false;
this._initialize();
}
static getInstance() {
if (!MediaSourceHelper._instance) {
MediaSourceHelper._instance = new MediaSourceHelper();
}
return MediaSourceHelper._instance;
}
_initialize() {
this._hasManagedMediaSource = 'ManagedMediaSource' in window;
const hasStandardMediaSource = 'MediaSource' in window;
this._isSupported = this._hasManagedMediaSource || hasStandardMediaSource;
if (this._isSupported) {
this._detectSupportedMimeTypes();
}
}
_detectSupportedMimeTypes() {
const commonMimeTypes = [
'audio/mpeg',
'audio/mp3',
'audio/wav',
'audio/webm',
'audio/webm; codecs=opus',
'audio/webm; codecs=vorbis',
'audio/ogg',
'audio/ogg; codecs=opus',
'audio/ogg; codecs=vorbis',
'audio/mp4',
'audio/mp4; codecs=mp4a.40.2',
'audio/aac'
];
this._supportedMimeTypes = commonMimeTypes.filter(mimeType => {
try {
return MediaSource.isTypeSupported(mimeType);
}
catch {
return false;
}
});
}
createMediaSource() {
if (!this._isSupported) {
throw new MediaSourceError('MediaSource API is not supported on this device');
}
let mediaSource;
let isManaged = false;
try {
if (this._hasManagedMediaSource) {
mediaSource = new window.ManagedMediaSource();
isManaged = true;
}
else {
mediaSource = new MediaSource();
isManaged = false;
}
}
catch (error) {
throw new MediaSourceError('Failed to create MediaSource instance', undefined, error);
}
return {
mediaSource,
isManaged,
supportedMimeTypes: [...this._supportedMimeTypes]
};
}
getBestMimeType(format) {
const preferredMimeTypes = this._getPreferredMimeTypesForFormat(format);
for (const mimeType of preferredMimeTypes) {
if (this._supportedMimeTypes.includes(mimeType)) {
return mimeType;
}
}
if (this._supportedMimeTypes.includes('audio/mpeg')) {
return 'audio/mpeg';
}
if (this._supportedMimeTypes.length > 0) {
return this._supportedMimeTypes[0];
}
throw new MediaSourceError(`No supported MIME type found for format: ${format.type}`);
}
_getPreferredMimeTypesForFormat(format) {
switch (format.type) {
case 'mp3':
return ['audio/mpeg', 'audio/mp3'];
case 'wav':
return ['audio/webm; codecs=opus', 'audio/mpeg'];
case 'webm':
return [
'audio/webm; codecs=opus',
'audio/webm; codecs=vorbis',
'audio/webm'
];
case 'ogg':
return [
'audio/ogg; codecs=opus',
'audio/ogg; codecs=vorbis',
'audio/ogg'
];
default:
return ['audio/mpeg'];
}
}
detectAudioFormat(data) {
const bytes = data instanceof ArrayBuffer ? new Uint8Array(data) : data;
if (this._isWavFormat(bytes)) {
return {
type: 'wav',
mimeType: 'audio/wav',
streamable: false,
requiresConversion: true
};
}
if (this._isMp3Format(bytes)) {
return {
type: 'mp3',
mimeType: 'audio/mpeg',
streamable: true
};
}
if (this._isWebMFormat(bytes)) {
return {
type: 'webm',
mimeType: 'audio/webm',
streamable: true,
codec: 'opus'
};
}
if (this._isOggFormat(bytes)) {
return {
type: 'ogg',
mimeType: 'audio/ogg',
streamable: true
};
}
return {
type: 'unknown',
mimeType: 'audio/mpeg',
streamable: false
};
}
_isWavFormat(bytes) {
if (bytes.length < 12)
return false;
const riff = String.fromCharCode(...bytes.slice(0, 4));
const wave = String.fromCharCode(...bytes.slice(8, 12));
return riff === 'RIFF' && wave === 'WAVE';
}
_isMp3Format(bytes) {
if (bytes.length < 3)
return false;
if (bytes[0] === 0x49 && bytes[1] === 0x44 && bytes[2] === 0x33) {
return true;
}
if (bytes.length >= 2 && bytes[0] === 0xFF && (bytes[1] & 0xE0) === 0xE0) {
return true;
}
if (bytes.length >= 128) {
const tagStart = bytes.length - 128;
if (bytes[tagStart] === 0x54 && bytes[tagStart + 1] === 0x41 && bytes[tagStart + 2] === 0x47) {
return true;
}
}
return false;
}
_isWebMFormat(bytes) {
if (bytes.length < 4)
return false;
return bytes[0] === 0x1A && bytes[1] === 0x45 && bytes[2] === 0xDF && bytes[3] === 0xA3;
}
_isOggFormat(bytes) {
if (bytes.length < 4)
return false;
const signature = String.fromCharCode(...bytes.slice(0, 4));
return signature === 'OggS';
}
async createSourceBuffer(mediaSource, mimeType, retryCount = 3) {
let lastError = null;
for (let attempt = 0; attempt < retryCount; attempt++) {
try {
const sourceBuffer = mediaSource.addSourceBuffer(mimeType);
return sourceBuffer;
}
catch (error) {
lastError = error;
if (attempt < retryCount - 1) {
await new Promise(resolve => setTimeout(resolve, 100 * (attempt + 1)));
}
}
}
throw new MediaSourceError(`Failed to create SourceBuffer with MIME type: ${mimeType}`, undefined, lastError);
}
async appendToSourceBuffer(sourceBuffer, data, timeout = 10000) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => {
reject(new MediaSourceError('SourceBuffer append timeout'));
}, timeout);
const onUpdateEnd = () => {
clearTimeout(timeoutId);
sourceBuffer.removeEventListener('updateend', onUpdateEnd);
sourceBuffer.removeEventListener('error', onError);
resolve();
};
const onError = (event) => {
clearTimeout(timeoutId);
sourceBuffer.removeEventListener('updateend', onUpdateEnd);
sourceBuffer.removeEventListener('error', onError);
let errorMessage = 'SourceBuffer append error';
if (event && event.error) {
errorMessage += `: ${event.error.message || 'Unknown error'}`;
}
errorMessage += '. This may be due to unsupported audio format or codec.';
reject(new MediaSourceError(errorMessage, undefined, event));
};
if (sourceBuffer.updating) {
sourceBuffer.addEventListener('updateend', () => {
this._performAppend(sourceBuffer, data, onUpdateEnd, onError);
}, { once: true });
}
else {
this._performAppend(sourceBuffer, data, onUpdateEnd, onError);
}
});
}
_performAppend(sourceBuffer, data, onUpdateEnd, onError) {
sourceBuffer.addEventListener('updateend', onUpdateEnd, { once: true });
sourceBuffer.addEventListener('error', onError, { once: true });
try {
const arrayBuffer = data instanceof Uint8Array
? data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)
: data;
sourceBuffer.appendBuffer(arrayBuffer);
}
catch (error) {
sourceBuffer.removeEventListener('updateend', onUpdateEnd);
sourceBuffer.removeEventListener('error', onError);
throw new MediaSourceError('Failed to append buffer', undefined, error);
}
}
getCapabilities() {
return {
isSupported: this._isSupported,
hasManagedMediaSource: this._hasManagedMediaSource,
supportedMimeTypes: [...this._supportedMimeTypes]
};
}
isMimeTypeSupported(mimeType) {
return this._supportedMimeTypes.includes(mimeType);
}
}
//# sourceMappingURL=MediaSourceHelper.js.map