/* * Copyright (c) 2016 Antoine Martin * Licensed under MPL 2.0 * */ const MediaSourceConstants = { CODEC_DESCRIPTION: { mp4a: "mpeg4: aac", "aac+mpeg4": "mpeg4: aac", mp3: "mp3", "mp3+id3v2": "mp3", "mp3+mpeg4": "mpeg4: mp3", wav: "wav", wave: "wave", flac: "flac", opus: "opus", vorbis: "vorbis", "opus+mka": "webm: opus", "opus+ogg": "ogg: opus", "vorbis+mka": "webm: vorbis", "vorbis+ogg": "ogg: vorbis", "speex+ogg": "ogg: speex", "flac+ogg": "ogg: flac", }, CODEC_STRING: { "aac+mpeg4": 'audio/mp4; codecs="mp4a.40.2"', mp3: "audio/mpeg", "mp3+mpeg4": 'audio/mp4; codecs="mp3"', ogg: "audio/ogg", wav: "audio/wav", flac: "audio/flac", "opus+mka": 'audio/webm; codecs="opus"', "vorbis+mka": 'audio/webm; codecs="vorbis"', "vorbis+ogg": 'audio/ogg; codecs="vorbis"', "speex+ogg": 'audio/ogg; codecs="speex"', "flac+ogg": 'audio/ogg; codecs="flac"', "opus+ogg": 'audio/ogg; codecs="opus"', }, PREFERRED_CODEC_ORDER: [ "opus+mka", "vorbis+mka", "opus+ogg", "vorbis+ogg", "opus", "vorbis", "speex+ogg", "flac+ogg", "aac+mpeg4", "mp3+mpeg4", "mp3", "mp3+id3v2", "flac", "wav", "wave", ], H264_PROFILE_CODE: { baseline: "42C0", main: "4D40", high: "6400", extended: "58A0", }, H264_LEVEL_CODE: { "3.0": "1E", 3.1: "1F", 4.1: "29", 5.1: "33", }, READY_STATE: { 0: "NOTHING", 1: "METADATA", 2: "CURRENT DATA", 3: "FUTURE DATA", 4: "ENOUGH DATA", }, NETWORK_STATE: { 0: "EMPTY", 1: "IDLE", 2: "LOADING", 3: "NO_SOURCE", }, ERROR_CODE: { 1: "ABORTED: fetching process aborted by user", 2: "NETWORK: error occurred when downloading", 3: "DECODE: error occurred when decoding", 4: "SRC_NOT_SUPPORTED", }, AURORA_CODECS: { wav: "lpcm", "mp3+id3v2": "mp3", flac: "flac", "aac+mpeg4": "mp4a", }, }; const MediaSourceUtil = { getMediaSourceClass() { return window.MediaSource || window.WebKitMediaSource; }, getMediaSource() { const ms = MediaSourceUtil.getMediaSourceClass(); if (!ms) { throw new Error("no MediaSource support!"); } return new ms(); }, getAuroraAudioCodecs() { const codecs_supported = new Map(); const codecs_failed = new Map(); if (AV && AV.Decoder && AV.Decoder.find) { for (const codec_option in MediaSourceConstants.AURORA_CODECS) { const codec_string = MediaSourceConstants.AURORA_CODECS[codec_option]; // eslint-disable-next-line unicorn/no-array-callback-reference const decoder = AV.Decoder.find(codec_string); if (decoder) { codecs_supported[codec_option] = codec_string; } else { codecs_failed[codec_option] = codec_string; } } } Utilities.log("audio aurora codecs supported:", codecs_supported); Utilities.log("audio aurora codecs not available:", codecs_failed); return codecs_supported; }, getMediaSourceAudioCodecs(ignore_blacklist) { const media_source_class = MediaSourceUtil.getMediaSourceClass(); if (!media_source_class) { Utilities.log("audio forwarding: no media source API support"); return []; } const codecs_supported = []; const codecs_failed = {}; for (const codec_option in MediaSourceConstants.CODEC_STRING) { const codec_string = MediaSourceConstants.CODEC_STRING[codec_option]; try { if (!media_source_class.isTypeSupported(codec_string)) { codecs_failed[codec_option] = codec_string; //add whitelisting here? continue; } let blacklist = []; if (Utilities.isFirefox() || Utilities.isSafari()) { blacklist += ["opus+mka", "vorbis+mka"]; if (Utilities.isSafari()) { //this crashes Safari! blacklist += ["wav"]; } } else if (Utilities.isChrome()) { blacklist = ["aac+mpeg4"]; if (Utilities.isMacOS()) { blacklist += ["opus+mka"]; } } if (blacklist.includes(codec_option)) { if (ignore_blacklist) { Utilities.log("blacklist overruled!"); } else { codecs_failed[codec_option] = codec_string; continue; } } codecs_supported[codec_option] = codec_string; } catch (error) { Utilities.error(`audio error probing codec '${codec_string}' / '${codec_string}': ${error}`); codecs_failed[codec_option] = codec_string; } } Utilities.log("audio codec MediaSource supported:", codecs_supported); Utilities.log("audio codec MediaSource not available:", codecs_failed); return codecs_supported; }, getSupportedAudioCodecs() { const codecs_supported = MediaSourceUtil.getMediaSourceAudioCodecs(); const aurora_codecs = MediaSourceUtil.getAuroraAudioCodecs(); for (const codec_option in aurora_codecs) { if (codec_option in codecs_supported) { //we already have native MediaSource support! continue; } codecs_supported[codec_option] = aurora_codecs[codec_option]; } return codecs_supported; }, getDefaultAudioCodec(codecs) { if (!codecs) { return null; } const codec_options = Object.keys(codecs); for (let codec_option of MediaSourceConstants.PREFERRED_CODEC_ORDER) { if (codec_options.includes(codec_option)) { return codec_option; } } return Object.keys(codecs)[0]; }, addMediaSourceEventDebugListeners(media_source, source_type) { function debug_source_event(event) { let message = `${source_type} source ${event}`; try { message += `: ${media_source.readyState}`; } catch { //don't care } console.debug(message); } media_source.addEventListener("sourceopen", function(e) { debug_source_event("open"); }); media_source.addEventListener("sourceended", function(e) { debug_source_event("ended"); }); media_source.addEventListener("sourceclose", function(e) { debug_source_event("close"); }); media_source.addEventListener("error", function(e) { debug_source_event("error"); }); }, addMediaElementEventDebugListeners(media_element, element_type) { function debug_me_event(event) { console.debug(`${element_type} ${event}`); } media_element.addEventListener("waiting", function() { debug_me_event("waiting"); }); media_element.addEventListener("stalled", function() { debug_me_event("stalled"); }); media_element.addEventListener("playing", function() { debug_me_event("playing"); }); media_element.addEventListener("loadstart", function() { debug_me_event("loadstart"); }); media_element.addEventListener("loadedmetadata", function() { debug_me_event("loadedmetadata"); }); media_element.addEventListener("loadeddata", function() { debug_me_event("loadeddata"); }); media_element.addEventListener("error", function() { debug_me_event("error"); }); media_element.addEventListener("canplay", function() { debug_me_event("canplay"); }); media_element.addEventListener("play", function() { debug_me_event("play"); }); }, addSourceBufferEventDebugListeners(asb, element_type) { function debug_buffer_event(event) { const message = `${element_type} buffer ${event}`; console.debug(message); } asb.addEventListener("updatestart", function(e) { debug_buffer_event("updatestart"); }); asb.addEventListener("updateend", function(e) { debug_buffer_event("updateend"); }); asb.addEventListener("error", function(e) { debug_buffer_event("error"); }); asb.addEventListener("abort", function(e) { debug_buffer_event("abort"); }); }, get_supported_codecs(mediasource, aurora, ignore_audio_blacklist) { const codecs_supported = {}; if (mediasource) { const mediasource_codecs = MediaSourceUtil.getMediaSourceAudioCodecs(ignore_audio_blacklist); for (const codec_option in mediasource_codecs) { codecs_supported[`mediasource:${codec_option}`] = MediaSourceConstants.CODEC_DESCRIPTION[codec_option]; } } if (aurora) { const aurora_codecs = MediaSourceUtil.getAuroraAudioCodecs(); for (const codec_option in aurora_codecs) { if (codec_option in codecs_supported) { //we already have native MediaSource support! continue; } codecs_supported[`aurora:${codec_option}`] = `legacy: ${MediaSourceConstants.CODEC_DESCRIPTION[codec_option]}`; } } return codecs_supported; }, get_best_codec(codecs_supported) { let best_codec = null; let best_distance = MediaSourceConstants.PREFERRED_CODEC_ORDER.length; for (const codec_option in codecs_supported) { const cs = codec_option.split(":")[1]; const distance = MediaSourceConstants.PREFERRED_CODEC_ORDER.indexOf(cs); if (distance >= 0 && distance < best_distance) { best_codec = codec_option; best_distance = distance; } } return best_codec; }, };