/* * noVNC: HTML5 VNC client * Copyright (C) 2019 The noVNC authors * Licensed under MPL 2.0 (see LICENSE.txt) * * See README.md for usage and integration instructions. * * Browser feature support detection */ import * as Log from './logging.js'; import Base64 from '../base64.js'; // Touch detection export let isTouchDevice = ('ontouchstart' in document.documentElement) || // requried for Chrome debugger (document.ontouchstart !== undefined) || // required for MS Surface (navigator.maxTouchPoints > 0) || (navigator.msMaxTouchPoints > 0); window.addEventListener('touchstart', function onFirstTouch() { isTouchDevice = true; window.removeEventListener('touchstart', onFirstTouch, false); }, false); // The goal is to find a certain physical width, the devicePixelRatio // brings us a bit closer but is not optimal. export let dragThreshold = 10 * (window.devicePixelRatio || 1); let _supportsCursorURIs = false; try { const target = document.createElement('canvas'); target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default'; if (target.style.cursor.indexOf("url") === 0) { Log.Info("Data URI scheme cursor supported"); _supportsCursorURIs = true; } else { Log.Warn("Data URI scheme cursor not supported"); } } catch (exc) { Log.Error("Data URI scheme cursor test exception: " + exc); } export const supportsCursorURIs = _supportsCursorURIs; let _hasScrollbarGutter = true; try { // Create invisible container const container = document.createElement('div'); container.style.visibility = 'hidden'; container.style.overflow = 'scroll'; // forcing scrollbars document.body.appendChild(container); // Create a div and place it in the container const child = document.createElement('div'); container.appendChild(child); // Calculate the difference between the container's full width // and the child's width - the difference is the scrollbars const scrollbarWidth = (container.offsetWidth - child.offsetWidth); // Clean up container.parentNode.removeChild(container); _hasScrollbarGutter = scrollbarWidth != 0; } catch (exc) { Log.Error("Scrollbar test exception: " + exc); } export const hasScrollbarGutter = _hasScrollbarGutter; export let supportsWebCodecsH264Decode = false; async function _checkWebCodecsH264DecodeSupport() { if (!('VideoDecoder' in window)) { return false; } // We'll need to make do with some placeholders here const config = { codec: 'avc1.42401f', codedWidth: 1920, codedHeight: 1080, optimizeForLatency: true, }; let support = await VideoDecoder.isConfigSupported(config); if (!support.supported) { return false; } // Firefox incorrectly reports supports for H.264 under some // circumstances, so we need to actually test a real frame // https://bugzilla.mozilla.org/show_bug.cgi?id=1932392 const data = new Uint8Array(Base64.decode( 'AAAAAWdCwBTZnpuAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//4Hc' + 'Rem95tlIt5Ys2CDZI+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5Zjkg' + 'LSBILjI2NC9NUEVHLTQgQVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDIz' + 'IC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1sIC0gb3B0aW9u' + 'czogY2FiYWM9MCByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5c2U9MHgxOjB4' + 'MTExIG1lPWhleCBzdWJtZT04IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4' + 'ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0yIDh4' + 'OGRjdD0wIGNxbT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJv' + 'bWFfcXBfb2Zmc2V0PS0yIHRocmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0x' + 'IHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9' + 'MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVz' + 'PTAgd2VpZ2h0cD0wIGtleWludD1pbmZpbml0ZSBrZXlpbnRfbWluPTI1IHNj' + 'ZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NTAgcmM9' + 'YWJyIG1idHJlZT0xIGJpdHJhdGU9NDAwIHJhdGV0b2w9MS4wIHFjb21wPTAu' + 'NjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFx' + 'PTE6MS4wMACAAAABZYiEBrxmKAAPVccAAS044AA5DRJMnkycJk4TPw==')); let gotframe = false; let error = null; let decoder = new VideoDecoder({ output: (frame) => { gotframe = true; }, error: (e) => { error = e; }, }); let chunk = new EncodedVideoChunk({ timestamp: 0, type: 'key', data: data, }); decoder.configure(config); decoder.decode(chunk); try { await decoder.flush(); } catch (e) { // Firefox incorrectly throws an exception here // https://bugzilla.mozilla.org/show_bug.cgi?id=1932566 error = e; } // Firefox fails to deliver the error on Windows, so we need to // check if we got a frame instead // https://bugzilla.mozilla.org/show_bug.cgi?id=1932579 if (!gotframe) { return false; } if (error !== null) { return false; } return true; } supportsWebCodecsH264Decode = await _checkWebCodecsH264DecodeSupport(); /* * The functions for detection of platforms and browsers below are exported * but the use of these should be minimized as much as possible. * * It's better to use feature detection than platform detection. */ /* OS */ export function isMac() { return !!(/mac/i).exec(navigator.platform); } export function isWindows() { return !!(/win/i).exec(navigator.platform); } export function isIOS() { return (!!(/ipad/i).exec(navigator.platform) || !!(/iphone/i).exec(navigator.platform) || !!(/ipod/i).exec(navigator.platform)); } export function isAndroid() { /* Android sets navigator.platform to Linux :/ */ return !!navigator.userAgent.match('Android '); } export function isChromeOS() { /* ChromeOS sets navigator.platform to Linux :/ */ return !!navigator.userAgent.match(' CrOS '); } /* Browser */ export function isSafari() { return !!navigator.userAgent.match('Safari/...') && !navigator.userAgent.match('Chrome/...') && !navigator.userAgent.match('Chromium/...') && !navigator.userAgent.match('Epiphany/...'); } export function isFirefox() { return !!navigator.userAgent.match('Firefox/...') && !navigator.userAgent.match('Seamonkey/...'); } export function isChrome() { return !!navigator.userAgent.match('Chrome/...') && !navigator.userAgent.match('Chromium/...') && !navigator.userAgent.match('Edg/...') && !navigator.userAgent.match('OPR/...'); } export function isChromium() { return !!navigator.userAgent.match('Chromium/...'); } export function isOpera() { return !!navigator.userAgent.match('OPR/...'); } export function isEdge() { return !!navigator.userAgent.match('Edg/...'); } /* Engine */ export function isGecko() { return !!navigator.userAgent.match('Gecko/...'); } export function isWebKit() { return !!navigator.userAgent.match('AppleWebKit/...') && !navigator.userAgent.match('Chrome/...'); } export function isBlink() { return !!navigator.userAgent.match('Chrome/...'); }