2026-04-21 13:16:36 +08:00

323 lines
9.2 KiB
JavaScript

/*
* Copyright (c) 2016 Antoine Martin <antoine@xpra.org>
* 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;
},
};