main
Raw Download raw file
  1/*
  2 * noVNC: HTML5 VNC client
  3 * Copyright (C) 2019 The noVNC authors
  4 * Licensed under MPL 2.0 (see LICENSE.txt)
  5 *
  6 * See README.md for usage and integration instructions.
  7 *
  8 * Browser feature support detection
  9 */
 10
 11import * as Log from './logging.js';
 12import Base64 from '../base64.js';
 13
 14// Async clipboard detection
 15
 16/* Evaluates if there is browser support for the async clipboard API and
 17 * relevant clipboard permissions. Returns 'unsupported' if permission states
 18 * cannot be resolved. On the other hand, detecting 'granted' or 'prompt'
 19 * permission states for both read and write indicates full API support with no
 20 * imposed native browser paste prompt. Conversely, detecting 'denied' indicates
 21 * the user elected to disable clipboard.
 22 */
 23export async function browserAsyncClipboardSupport() {
 24    if (!(navigator?.permissions?.query &&
 25          navigator?.clipboard?.writeText &&
 26          navigator?.clipboard?.readText)) {
 27        return 'unsupported';
 28    }
 29    try {
 30        const writePerm = await navigator.permissions.query(
 31            {name: "clipboard-write", allowWithoutGesture: true});
 32        const readPerm = await navigator.permissions.query(
 33            {name: "clipboard-read",  allowWithoutGesture: false});
 34        if (writePerm.state === "denied" || readPerm.state  === "denied") {
 35            return 'denied';
 36        }
 37        if ((writePerm.state === "granted" || writePerm.state === "prompt") &&
 38            (readPerm.state  === "granted" || readPerm.state  === "prompt")) {
 39            return 'available';
 40        }
 41    } catch {
 42        return 'unsupported';
 43    }
 44    return 'unsupported';
 45}
 46
 47// Touch detection
 48export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
 49                                 // required for Chrome debugger
 50                                 (document.ontouchstart !== undefined) ||
 51                                 // required for MS Surface
 52                                 (navigator.maxTouchPoints > 0) ||
 53                                 (navigator.msMaxTouchPoints > 0);
 54window.addEventListener('touchstart', function onFirstTouch() {
 55    isTouchDevice = true;
 56    window.removeEventListener('touchstart', onFirstTouch, false);
 57}, false);
 58
 59
 60// The goal is to find a certain physical width, the devicePixelRatio
 61// brings us a bit closer but is not optimal.
 62export let dragThreshold = 10 * (window.devicePixelRatio || 1);
 63
 64let _supportsCursorURIs = false;
 65
 66try {
 67    const target = document.createElement('canvas');
 68    target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default';
 69
 70    if (target.style.cursor.indexOf("url") === 0) {
 71        Log.Info("Data URI scheme cursor supported");
 72        _supportsCursorURIs = true;
 73    } else {
 74        Log.Warn("Data URI scheme cursor not supported");
 75    }
 76} catch (exc) {
 77    Log.Error("Data URI scheme cursor test exception: " + exc);
 78}
 79
 80export const supportsCursorURIs = _supportsCursorURIs;
 81
 82let _hasScrollbarGutter = true;
 83try {
 84    // Create invisible container
 85    const container = document.createElement('div');
 86    container.style.visibility = 'hidden';
 87    container.style.overflow = 'scroll'; // forcing scrollbars
 88    document.body.appendChild(container);
 89
 90    // Create a div and place it in the container
 91    const child = document.createElement('div');
 92    container.appendChild(child);
 93
 94    // Calculate the difference between the container's full width
 95    // and the child's width - the difference is the scrollbars
 96    const scrollbarWidth = (container.offsetWidth - child.offsetWidth);
 97
 98    // Clean up
 99    container.parentNode.removeChild(container);
100
101    _hasScrollbarGutter = scrollbarWidth != 0;
102} catch (exc) {
103    Log.Error("Scrollbar test exception: " + exc);
104}
105export const hasScrollbarGutter = _hasScrollbarGutter;
106
107export let supportsWebCodecsH264Decode = false;
108
109async function _checkWebCodecsH264DecodeSupport() {
110    if (!('VideoDecoder' in window)) {
111        return false;
112    }
113
114    // We'll need to make do with some placeholders here
115    const config = {
116        codec: 'avc1.42401f',
117        codedWidth: 1920,
118        codedHeight: 1080,
119        optimizeForLatency: true,
120    };
121
122    let support = await VideoDecoder.isConfigSupported(config);
123    if (!support.supported) {
124        return false;
125    }
126
127    // Firefox incorrectly reports supports for H.264 under some
128    // circumstances, so we need to actually test a real frame
129    // https://bugzilla.mozilla.org/show_bug.cgi?id=1932392
130
131    const data = new Uint8Array(Base64.decode(
132        'AAAAAWdCwBTZnpuAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//4Hc' +
133        'Rem95tlIt5Ys2CDZI+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5Zjkg' +
134        'LSBILjI2NC9NUEVHLTQgQVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDIz' +
135        'IC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1sIC0gb3B0aW9u' +
136        'czogY2FiYWM9MCByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5c2U9MHgxOjB4' +
137        'MTExIG1lPWhleCBzdWJtZT04IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4' +
138        'ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0yIDh4' +
139        'OGRjdD0wIGNxbT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJv' +
140        'bWFfcXBfb2Zmc2V0PS0yIHRocmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0x' +
141        'IHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9' +
142        'MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVz' +
143        'PTAgd2VpZ2h0cD0wIGtleWludD1pbmZpbml0ZSBrZXlpbnRfbWluPTI1IHNj' +
144        'ZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NTAgcmM9' +
145        'YWJyIG1idHJlZT0xIGJpdHJhdGU9NDAwIHJhdGV0b2w9MS4wIHFjb21wPTAu' +
146        'NjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFx' +
147        'PTE6MS4wMACAAAABZYiEBrxmKAAPVccAAS044AA5DRJMnkycJk4TPw=='));
148
149    let gotframe = false;
150    let error = null;
151
152    let decoder = new VideoDecoder({
153        output: (frame) => { gotframe = true; frame.close(); },
154        error: (e) => { error = e; },
155    });
156    let chunk = new EncodedVideoChunk({
157        timestamp: 0,
158        type: 'key',
159        data: data,
160    });
161
162    decoder.configure(config);
163    decoder.decode(chunk);
164    try {
165        await decoder.flush();
166    } catch (e) {
167        // Firefox incorrectly throws an exception here
168        // https://bugzilla.mozilla.org/show_bug.cgi?id=1932566
169        error = e;
170    }
171
172    // Firefox fails to deliver the error on Windows, so we need to
173    // check if we got a frame instead
174    // https://bugzilla.mozilla.org/show_bug.cgi?id=1932579
175    if (!gotframe) {
176        return false;
177    }
178
179    if (error !== null) {
180        return false;
181    }
182
183    return true;
184}
185supportsWebCodecsH264Decode = await _checkWebCodecsH264DecodeSupport();
186
187/*
188 * The functions for detection of platforms and browsers below are exported
189 * but the use of these should be minimized as much as possible.
190 *
191 * It's better to use feature detection than platform detection.
192 */
193
194/* OS */
195
196export function isMac() {
197    return !!(/mac/i).exec(navigator.platform);
198}
199
200export function isWindows() {
201    return !!(/win/i).exec(navigator.platform);
202}
203
204export function isIOS() {
205    return (!!(/ipad/i).exec(navigator.platform) ||
206            !!(/iphone/i).exec(navigator.platform) ||
207            !!(/ipod/i).exec(navigator.platform));
208}
209
210export function isAndroid() {
211    /* Android sets navigator.platform to Linux :/ */
212    return !!navigator.userAgent.match('Android ');
213}
214
215export function isChromeOS() {
216    /* ChromeOS sets navigator.platform to Linux :/ */
217    return !!navigator.userAgent.match(' CrOS ');
218}
219
220/* Browser */
221
222export function isSafari() {
223    return !!navigator.userAgent.match('Safari/...') &&
224           !navigator.userAgent.match('Chrome/...') &&
225           !navigator.userAgent.match('Chromium/...') &&
226           !navigator.userAgent.match('Epiphany/...');
227}
228
229export function isFirefox() {
230    return !!navigator.userAgent.match('Firefox/...') &&
231           !navigator.userAgent.match('Seamonkey/...');
232}
233
234export function isChrome() {
235    return !!navigator.userAgent.match('Chrome/...') &&
236           !navigator.userAgent.match('Chromium/...') &&
237           !navigator.userAgent.match('Edg/...') &&
238           !navigator.userAgent.match('OPR/...');
239}
240
241export function isChromium() {
242    return !!navigator.userAgent.match('Chromium/...');
243}
244
245export function isOpera() {
246    return !!navigator.userAgent.match('OPR/...');
247}
248
249export function isEdge() {
250    return !!navigator.userAgent.match('Edg/...');
251}
252
253/* Engine */
254
255export function isGecko() {
256    return !!navigator.userAgent.match('Gecko/...');
257}
258
259export function isWebKit() {
260    return !!navigator.userAgent.match('AppleWebKit/...') &&
261           !navigator.userAgent.match('Chrome/...');
262}
263
264export function isBlink() {
265    return !!navigator.userAgent.match('Chrome/...');
266}