Skip to content

Commit ef382b7

Browse files
Add PoC support for device enum
1 parent a823caa commit ef382b7

File tree

6 files changed

+234
-1
lines changed

6 files changed

+234
-1
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { gotoAndWait, testContextForExtension } from './helpers/harness.js';
2+
import { test as base, expect } from '@playwright/test';
3+
4+
const test = testContextForExtension(base);
5+
6+
test.describe('Device Enumeration Feature', () => {
7+
test.describe('disabled feature', () => {
8+
test('should not intercept enumerateDevices when disabled', async ({ page }) => {
9+
await gotoAndWait(page, '/webcompat/pages/device-enumeration.html', {
10+
site: { enabledFeatures: [] },
11+
});
12+
13+
// Should use native implementation
14+
const results = await page.evaluate(() => {
15+
// @ts-expect-error - results is set by renderResults()
16+
return window.results;
17+
});
18+
19+
// The test should pass with native behavior
20+
expect(results).toBeDefined();
21+
});
22+
});
23+
24+
test.describe('enabled feature', () => {
25+
test('should intercept enumerateDevices when enabled', async ({ page }) => {
26+
await gotoAndWait(page, '/webcompat/pages/device-enumeration.html', {
27+
site: {
28+
enabledFeatures: ['webCompat'],
29+
},
30+
featureSettings: {
31+
webCompat: {
32+
deviceEnumeration: 'enabled',
33+
},
34+
},
35+
});
36+
37+
// Should use our implementation
38+
const results = await page.evaluate(() => {
39+
// @ts-expect-error - results is set by renderResults()
40+
return window.results;
41+
});
42+
43+
// The test should pass with our implementation
44+
expect(results).toBeDefined();
45+
});
46+
});
47+
});
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
"readme": "This config is used to test the device enumeration feature.",
3+
"version": 1,
4+
"unprotectedTemporary": [],
5+
"features": {
6+
"webCompat": {
7+
"state": "enabled",
8+
"exceptions": [],
9+
"settings": {
10+
"deviceEnumeration": "enabled"
11+
}
12+
}
13+
}
14+
}

injected/integration-test/test-pages/webcompat/index.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<li><a href="./pages/message-handlers.html">Message Handlers</a> - <a href="./config/message-handlers.json">Config</a></li>
1313
<li><a href="./pages/shims.html">Shims</a> - <a href="./config/shims.json">Config</a></li>
1414
<li><a href="./pages/modify-localstorage.html">Modify localStorage</a> - <a href="./config/modify-localstorage.json">Config</a></li>
15+
<li><a href="./pages/device-enumeration.html">Device Enumeration</a> - <a href="./config/device-enumeration.json">Config</a></li>
1516
</ul>
1617
</body>
1718
</html>
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta charset="utf-8">
5+
<meta name="viewport" content="width=device-width">
6+
<title>Device Enumeration Test</title>
7+
<link rel="stylesheet" href="../../shared/style.css">
8+
</head>
9+
<body>
10+
<script src="../../shared/utils.js"></script>
11+
<p><a href="../index.html">[Webcompat shims]</a></p>
12+
13+
<p>This page tests the device enumeration feature</p>
14+
15+
<script>
16+
test('Device Enumeration', async () => {
17+
if (!navigator.mediaDevices) {
18+
return [
19+
{
20+
name: 'MediaDevices not available',
21+
result: 'MediaDevices API not supported',
22+
expected: 'MediaDevices API not supported'
23+
}
24+
];
25+
}
26+
27+
try {
28+
const devices = await navigator.mediaDevices.enumerateDevices();
29+
30+
// Check if we got a valid response
31+
const hasVideoInput = devices.some(device => device.kind === 'videoinput');
32+
const hasAudioInput = devices.some(device => device.kind === 'audioinput');
33+
const hasAudioOutput = devices.some(device => device.kind === 'audiooutput');
34+
35+
return [
36+
{
37+
name: 'enumerateDevices returns array',
38+
result: Array.isArray(devices),
39+
expected: true
40+
},
41+
{
42+
name: 'devices have correct structure',
43+
result: devices.every(device =>
44+
typeof device.deviceId === 'string' &&
45+
typeof device.kind === 'string' &&
46+
typeof device.label === 'string' &&
47+
typeof device.groupId === 'string'
48+
),
49+
expected: true
50+
},
51+
{
52+
name: 'video input devices present',
53+
result: hasVideoInput,
54+
expected: true
55+
},
56+
{
57+
name: 'audio input devices present',
58+
result: hasAudioInput,
59+
expected: true
60+
},
61+
{
62+
name: 'audio output devices present',
63+
result: hasAudioOutput,
64+
expected: true
65+
}
66+
];
67+
} catch (error) {
68+
return [
69+
{
70+
name: 'enumerateDevices throws error',
71+
result: error.message,
72+
expected: 'Should not throw error'
73+
}
74+
];
75+
}
76+
});
77+
78+
renderResults();
79+
</script>
80+
</body>
81+
</html>

injected/src/features/web-compat.js

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import ContentFeature from '../content-feature.js';
22
// eslint-disable-next-line no-redeclare
33
import { URL } from '../captured-globals.js';
4-
import { DDGProxy } from '../utils';
4+
import { DDGProxy, DDGReflect } from '../utils';
55
/**
66
* Fixes incorrect sizing value for outerHeight and outerWidth
77
*/
@@ -17,6 +17,7 @@ const MSG_WEB_SHARE = 'webShare';
1717
const MSG_PERMISSIONS_QUERY = 'permissionsQuery';
1818
const MSG_SCREEN_LOCK = 'screenLock';
1919
const MSG_SCREEN_UNLOCK = 'screenUnlock';
20+
const MSG_DEVICE_ENUMERATION = 'deviceEnumeration';
2021

2122
function canShare(data) {
2223
if (typeof data !== 'object') return false;
@@ -129,6 +130,9 @@ export class WebCompat extends ContentFeature {
129130
if (this.getFeatureSettingEnabled('disableDeviceEnumeration') || this.getFeatureSettingEnabled('disableDeviceEnumerationFrames')) {
130131
this.preventDeviceEnumeration();
131132
}
133+
if (this.getFeatureSettingEnabled('deviceEnumeration')) {
134+
this.deviceEnumerationFix();
135+
}
132136
}
133137

134138
/** Shim Web Share API in Android WebView */
@@ -777,6 +781,65 @@ export class WebCompat extends ContentFeature {
777781
enumerateDevicesProxy.overload();
778782
}
779783
}
784+
785+
deviceEnumerationFix() {
786+
if (!window.MediaDevices) {
787+
return;
788+
}
789+
790+
const enumerateDevicesProxy = new DDGProxy(this, MediaDevices.prototype, 'enumerateDevices', {
791+
apply: async (target, thisArg, args) => {
792+
try {
793+
// Request device enumeration information from native
794+
const response = await this.messaging.request(MSG_DEVICE_ENUMERATION, {});
795+
796+
// Check if native indicates that prompts would be required
797+
if (response.willPrompt) {
798+
// If prompts would be required, return a manipulated response
799+
// that includes the device types that are available
800+
const devices = [];
801+
802+
if (response.videoInput) {
803+
devices.push({
804+
deviceId: 'default',
805+
kind: 'videoinput',
806+
label: '',
807+
groupId: 'default-group',
808+
});
809+
}
810+
811+
if (response.audioInput) {
812+
devices.push({
813+
deviceId: 'default',
814+
kind: 'audioinput',
815+
label: '',
816+
groupId: 'default-group',
817+
});
818+
}
819+
820+
if (response.audioOutput) {
821+
devices.push({
822+
deviceId: 'default',
823+
kind: 'audiooutput',
824+
label: '',
825+
groupId: 'default-group',
826+
});
827+
}
828+
829+
return Promise.resolve(devices);
830+
} else {
831+
// If no prompts would be required, proceed with the regular device enumeration
832+
return DDGReflect.apply(target, thisArg, args);
833+
}
834+
} catch (err) {
835+
// If the native request fails, fall back to the original implementation
836+
return DDGReflect.apply(target, thisArg, args);
837+
}
838+
},
839+
});
840+
841+
enumerateDevicesProxy.overload();
842+
}
780843
}
781844

782845
/** @typedef {{title?: string, url?: string, text?: string}} ShareRequestData */
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"description": "Request device enumeration information from native layer",
3+
"params": {},
4+
"response": {
5+
"description": "Device enumeration information from native layer",
6+
"properties": {
7+
"videoInput": {
8+
"description": "Whether video input devices are available",
9+
"type": "boolean"
10+
},
11+
"audioInput": {
12+
"description": "Whether audio input devices are available",
13+
"type": "boolean"
14+
},
15+
"audioOutput": {
16+
"description": "Whether audio output devices are available",
17+
"type": "boolean"
18+
},
19+
"willPrompt": {
20+
"description": "Whether the API would prompt for permissions",
21+
"type": "boolean"
22+
}
23+
},
24+
"required": ["videoInput", "audioInput", "audioOutput", "willPrompt"],
25+
"type": "object"
26+
}
27+
}

0 commit comments

Comments
 (0)