@ricardodeazambuja/browser-mcp-server 1.0.3 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG-v1.3.0.md +42 -0
- package/CHANGELOG-v1.4.0.md +8 -0
- package/README.md +271 -45
- package/package.json +11 -10
- package/plugins/.gitkeep +0 -0
- package/src/.gitkeep +0 -0
- package/src/browser.js +152 -0
- package/src/cdp.js +58 -0
- package/src/index.js +126 -0
- package/src/tools/.gitkeep +0 -0
- package/src/tools/console.js +139 -0
- package/src/tools/docs.js +1611 -0
- package/src/tools/index.js +60 -0
- package/src/tools/info.js +139 -0
- package/src/tools/interaction.js +126 -0
- package/src/tools/keyboard.js +27 -0
- package/src/tools/media.js +264 -0
- package/src/tools/mouse.js +104 -0
- package/src/tools/navigation.js +72 -0
- package/src/tools/network.js +552 -0
- package/src/tools/pages.js +149 -0
- package/src/tools/performance.js +517 -0
- package/src/tools/security.js +470 -0
- package/src/tools/storage.js +467 -0
- package/src/tools/system.js +196 -0
- package/src/utils.js +131 -0
- package/tests/.gitkeep +0 -0
- package/tests/fixtures/.gitkeep +0 -0
- package/tests/fixtures/test-media.html +35 -0
- package/tests/fixtures/test-network.html +48 -0
- package/tests/fixtures/test-performance.html +61 -0
- package/tests/fixtures/test-security.html +33 -0
- package/tests/fixtures/test-storage.html +76 -0
- package/tests/run-all.js +50 -0
- package/{test-browser-automation.js → tests/test-browser-automation.js} +44 -5
- package/{test-mcp.js → tests/test-mcp.js} +9 -4
- package/tests/test-media-tools.js +168 -0
- package/tests/test-network.js +212 -0
- package/tests/test-performance.js +254 -0
- package/tests/test-security.js +203 -0
- package/tests/test-storage.js +192 -0
- package/CHANGELOG-v1.0.2.md +0 -126
- package/browser-mcp-server-playwright.js +0 -792
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
const tools = [];
|
|
5
|
+
const handlers = {};
|
|
6
|
+
|
|
7
|
+
// Default tool modules to load
|
|
8
|
+
const coreModules = [
|
|
9
|
+
'navigation',
|
|
10
|
+
'interaction',
|
|
11
|
+
'mouse',
|
|
12
|
+
'keyboard',
|
|
13
|
+
'pages',
|
|
14
|
+
'media',
|
|
15
|
+
'console',
|
|
16
|
+
'info',
|
|
17
|
+
'system',
|
|
18
|
+
'docs',
|
|
19
|
+
'performance',
|
|
20
|
+
'network',
|
|
21
|
+
'security',
|
|
22
|
+
'storage'
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Load core tools
|
|
26
|
+
coreModules.forEach(name => {
|
|
27
|
+
try {
|
|
28
|
+
const mod = require(`./${name}.js`);
|
|
29
|
+
if (mod.definitions) {
|
|
30
|
+
tools.push(...mod.definitions);
|
|
31
|
+
}
|
|
32
|
+
if (mod.handlers) {
|
|
33
|
+
Object.assign(handlers, mod.handlers);
|
|
34
|
+
}
|
|
35
|
+
} catch (error) {
|
|
36
|
+
console.error(`Failed to load core tool module: ${name}`, error);
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Auto-load plugins from ../../plugins/ if they exist
|
|
41
|
+
const pluginsDir = path.join(__dirname, '..', '..', 'plugins');
|
|
42
|
+
if (fs.existsSync(pluginsDir)) {
|
|
43
|
+
fs.readdirSync(pluginsDir)
|
|
44
|
+
.filter(f => f.endsWith('.js'))
|
|
45
|
+
.forEach(f => {
|
|
46
|
+
try {
|
|
47
|
+
const mod = require(path.join(pluginsDir, f));
|
|
48
|
+
if (mod.definitions) {
|
|
49
|
+
tools.push(...mod.definitions);
|
|
50
|
+
}
|
|
51
|
+
if (mod.handlers) {
|
|
52
|
+
Object.assign(handlers, mod.handlers);
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(`Failed to load plugin: ${f}`, error);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { tools, handlers };
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const { connectToBrowser } = require('../browser');
|
|
2
|
+
|
|
3
|
+
const definitions = [
|
|
4
|
+
{
|
|
5
|
+
name: 'browser_screenshot',
|
|
6
|
+
description: 'Take a screenshot of the current page (see browser_docs)',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
fullPage: { type: 'boolean', description: 'Capture full page', default: false }
|
|
11
|
+
},
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
name: 'browser_get_text',
|
|
18
|
+
description: 'Get text content from an element (see browser_docs)',
|
|
19
|
+
inputSchema: {
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
selector: { type: 'string', description: 'Playwright selector for the element' }
|
|
23
|
+
},
|
|
24
|
+
required: ['selector'],
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
name: 'browser_evaluate',
|
|
31
|
+
description: 'Execute JavaScript in the browser context (see browser_docs)',
|
|
32
|
+
inputSchema: {
|
|
33
|
+
type: 'object',
|
|
34
|
+
properties: {
|
|
35
|
+
code: { type: 'string', description: 'JavaScript code to execute' }
|
|
36
|
+
},
|
|
37
|
+
required: ['code'],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'browser_get_dom',
|
|
44
|
+
description: 'Get the full DOM structure or specific element data (see browser_docs)',
|
|
45
|
+
inputSchema: {
|
|
46
|
+
type: 'object',
|
|
47
|
+
properties: {
|
|
48
|
+
selector: { type: 'string', description: 'Optional selector to get DOM of specific element' }
|
|
49
|
+
},
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'browser_read_page',
|
|
56
|
+
description: 'Read the content and metadata of the current page (see browser_docs)',
|
|
57
|
+
inputSchema: {
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {},
|
|
60
|
+
additionalProperties: false,
|
|
61
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
const handlers = {
|
|
67
|
+
browser_screenshot: async (args) => {
|
|
68
|
+
const { page } = await connectToBrowser();
|
|
69
|
+
const screenshot = await page.screenshot({
|
|
70
|
+
fullPage: args.fullPage || false,
|
|
71
|
+
type: 'png'
|
|
72
|
+
});
|
|
73
|
+
return {
|
|
74
|
+
content: [{
|
|
75
|
+
type: 'image',
|
|
76
|
+
data: screenshot.toString('base64'),
|
|
77
|
+
mimeType: 'image/png'
|
|
78
|
+
}]
|
|
79
|
+
};
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
browser_get_text: async (args) => {
|
|
83
|
+
const { page } = await connectToBrowser();
|
|
84
|
+
const text = await page.textContent(args.selector);
|
|
85
|
+
return { content: [{ type: 'text', text }] };
|
|
86
|
+
},
|
|
87
|
+
|
|
88
|
+
browser_evaluate: async (args) => {
|
|
89
|
+
const { page } = await connectToBrowser();
|
|
90
|
+
const result = await page.evaluate(args.code);
|
|
91
|
+
return {
|
|
92
|
+
content: [{
|
|
93
|
+
type: 'text',
|
|
94
|
+
text: JSON.stringify(result, null, 2)
|
|
95
|
+
}]
|
|
96
|
+
};
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
browser_get_dom: async (args) => {
|
|
100
|
+
const { page } = await connectToBrowser();
|
|
101
|
+
const domContent = await page.evaluate((sel) => {
|
|
102
|
+
const element = sel ? document.querySelector(sel) : document.documentElement;
|
|
103
|
+
if (!element) return null;
|
|
104
|
+
return {
|
|
105
|
+
outerHTML: element.outerHTML,
|
|
106
|
+
textContent: element.textContent,
|
|
107
|
+
attributes: Array.from(element.attributes || []).map(attr => ({
|
|
108
|
+
name: attr.name,
|
|
109
|
+
value: attr.value
|
|
110
|
+
})),
|
|
111
|
+
children: element.children.length
|
|
112
|
+
};
|
|
113
|
+
}, args.selector);
|
|
114
|
+
return {
|
|
115
|
+
content: [{
|
|
116
|
+
type: 'text',
|
|
117
|
+
text: JSON.stringify(domContent, null, 2)
|
|
118
|
+
}]
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
browser_read_page: async (args) => {
|
|
123
|
+
const { page } = await connectToBrowser();
|
|
124
|
+
const metadata = {
|
|
125
|
+
title: await page.title(),
|
|
126
|
+
url: page.url(),
|
|
127
|
+
viewport: page.viewportSize(),
|
|
128
|
+
contentLength: (await page.content()).length
|
|
129
|
+
};
|
|
130
|
+
return {
|
|
131
|
+
content: [{
|
|
132
|
+
type: 'text',
|
|
133
|
+
text: JSON.stringify(metadata, null, 2)
|
|
134
|
+
}]
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
module.exports = { definitions, handlers };
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const { connectToBrowser } = require('../browser');
|
|
2
|
+
|
|
3
|
+
const definitions = [
|
|
4
|
+
{
|
|
5
|
+
name: 'browser_click',
|
|
6
|
+
description: 'Click an element on the page using Playwright selector (see browser_docs)',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
selector: { type: 'string', description: 'Playwright selector for the element' }
|
|
11
|
+
},
|
|
12
|
+
required: ['selector'],
|
|
13
|
+
additionalProperties: false,
|
|
14
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: 'browser_type',
|
|
19
|
+
description: 'Type text into an input field (see browser_docs)',
|
|
20
|
+
inputSchema: {
|
|
21
|
+
type: 'object',
|
|
22
|
+
properties: {
|
|
23
|
+
selector: { type: 'string', description: 'Playwright selector for the input' },
|
|
24
|
+
text: { type: 'string', description: 'Text to type' }
|
|
25
|
+
},
|
|
26
|
+
required: ['selector', 'text'],
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'browser_hover',
|
|
33
|
+
description: 'Hover over an element (see browser_docs)',
|
|
34
|
+
inputSchema: {
|
|
35
|
+
type: 'object',
|
|
36
|
+
properties: {
|
|
37
|
+
selector: { type: 'string', description: 'Playwright selector for the element' }
|
|
38
|
+
},
|
|
39
|
+
required: ['selector'],
|
|
40
|
+
additionalProperties: false,
|
|
41
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: 'browser_focus',
|
|
46
|
+
description: 'Focus an element (see browser_docs)',
|
|
47
|
+
inputSchema: {
|
|
48
|
+
type: 'object',
|
|
49
|
+
properties: {
|
|
50
|
+
selector: { type: 'string', description: 'Playwright selector for the element' }
|
|
51
|
+
},
|
|
52
|
+
required: ['selector'],
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'browser_select',
|
|
59
|
+
description: 'Select options in a dropdown (see browser_docs)',
|
|
60
|
+
inputSchema: {
|
|
61
|
+
type: 'object',
|
|
62
|
+
properties: {
|
|
63
|
+
selector: { type: 'string', description: 'Playwright selector for the select element' },
|
|
64
|
+
values: { type: 'array', items: { type: 'string' }, description: 'Values to select' }
|
|
65
|
+
},
|
|
66
|
+
required: ['selector', 'values'],
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
name: 'browser_scroll',
|
|
73
|
+
description: 'Scroll the page (see browser_docs)',
|
|
74
|
+
inputSchema: {
|
|
75
|
+
type: 'object',
|
|
76
|
+
properties: {
|
|
77
|
+
x: { type: 'number', description: 'Horizontal scroll position' },
|
|
78
|
+
y: { type: 'number', description: 'Vertical scroll position' }
|
|
79
|
+
},
|
|
80
|
+
additionalProperties: false,
|
|
81
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
const handlers = {
|
|
87
|
+
browser_click: async (args) => {
|
|
88
|
+
const { page } = await connectToBrowser();
|
|
89
|
+
await page.click(args.selector);
|
|
90
|
+
return { content: [{ type: 'text', text: `Clicked ${args.selector}` }] };
|
|
91
|
+
},
|
|
92
|
+
browser_type: async (args) => {
|
|
93
|
+
const { page } = await connectToBrowser();
|
|
94
|
+
await page.fill(args.selector, args.text);
|
|
95
|
+
return { content: [{ type: 'text', text: `Typed into ${args.selector}` }] };
|
|
96
|
+
},
|
|
97
|
+
browser_hover: async (args) => {
|
|
98
|
+
const { page } = await connectToBrowser();
|
|
99
|
+
await page.hover(args.selector);
|
|
100
|
+
return { content: [{ type: 'text', text: `Hovered over ${args.selector}` }] };
|
|
101
|
+
},
|
|
102
|
+
browser_focus: async (args) => {
|
|
103
|
+
const { page } = await connectToBrowser();
|
|
104
|
+
await page.focus(args.selector);
|
|
105
|
+
return { content: [{ type: 'text', text: `Focused ${args.selector}` }] };
|
|
106
|
+
},
|
|
107
|
+
browser_select: async (args) => {
|
|
108
|
+
const { page } = await connectToBrowser();
|
|
109
|
+
await page.selectOption(args.selector, args.values);
|
|
110
|
+
return { content: [{ type: 'text', text: `Selected values in ${args.selector}` }] };
|
|
111
|
+
},
|
|
112
|
+
browser_scroll: async (args) => {
|
|
113
|
+
const { page } = await connectToBrowser();
|
|
114
|
+
await page.evaluate(({ x, y }) => {
|
|
115
|
+
window.scrollTo(x || 0, y || 0);
|
|
116
|
+
}, args);
|
|
117
|
+
return {
|
|
118
|
+
content: [{
|
|
119
|
+
type: 'text',
|
|
120
|
+
text: `Scrolled to (${args.x || 0}, ${args.y || 0})`
|
|
121
|
+
}]
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
module.exports = { definitions, handlers };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const { connectToBrowser } = require('../browser');
|
|
2
|
+
|
|
3
|
+
const definitions = [
|
|
4
|
+
{
|
|
5
|
+
name: 'browser_press_key',
|
|
6
|
+
description: 'Send a keyboard event (press a key) (see browser_docs)',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {
|
|
10
|
+
key: { type: 'string', description: 'The key to press (e.g., "Enter", "Escape", "Control+A")' }
|
|
11
|
+
},
|
|
12
|
+
required: ['key'],
|
|
13
|
+
additionalProperties: false,
|
|
14
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const handlers = {
|
|
20
|
+
browser_press_key: async (args) => {
|
|
21
|
+
const { page } = await connectToBrowser();
|
|
22
|
+
await page.keyboard.press(args.key);
|
|
23
|
+
return { content: [{ type: 'text', text: `Pressed key: ${args.key}` }] };
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
module.exports = { definitions, handlers };
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
const { connectToBrowser } = require('../browser');
|
|
2
|
+
|
|
3
|
+
const definitions = [
|
|
4
|
+
{
|
|
5
|
+
name: 'browser_get_media_summary',
|
|
6
|
+
description: 'Get a summary of all audio and video elements on the page (see browser_docs)',
|
|
7
|
+
inputSchema: {
|
|
8
|
+
type: 'object',
|
|
9
|
+
properties: {},
|
|
10
|
+
additionalProperties: false,
|
|
11
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: 'browser_get_audio_analysis',
|
|
16
|
+
description: 'Analyze audio output for a duration to detect sound vs silence and frequencies (see browser_docs)',
|
|
17
|
+
inputSchema: {
|
|
18
|
+
type: 'object',
|
|
19
|
+
properties: {
|
|
20
|
+
durationMs: { type: 'number', description: 'Duration to analyze in ms', default: 2000 },
|
|
21
|
+
selector: { type: 'string', description: 'Optional selector to specific media element' }
|
|
22
|
+
},
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: 'browser_control_media',
|
|
29
|
+
description: 'Control a media element (play, pause, seek, mute) (see browser_docs)',
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
selector: { type: 'string', description: 'Selector for the audio/video element' },
|
|
34
|
+
action: { type: 'string', enum: ['play', 'pause', 'mute', 'unmute', 'seek'] },
|
|
35
|
+
value: { type: 'number', description: 'Value for seek action (time in seconds)' }
|
|
36
|
+
},
|
|
37
|
+
required: ['selector', 'action'],
|
|
38
|
+
additionalProperties: false,
|
|
39
|
+
$schema: 'http://json-schema.org/draft-07/schema#'
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
const handlers = {
|
|
45
|
+
browser_get_media_summary: async (args) => {
|
|
46
|
+
const { page } = await connectToBrowser();
|
|
47
|
+
const mediaState = await page.evaluate(() => {
|
|
48
|
+
const elements = Array.from(document.querySelectorAll('audio, video'));
|
|
49
|
+
return elements.map((el, index) => {
|
|
50
|
+
// Calculate buffered ranges
|
|
51
|
+
const buffered = [];
|
|
52
|
+
for (let i = 0; i < el.buffered.length; i++) {
|
|
53
|
+
buffered.push([el.buffered.start(i), el.buffered.end(i)]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
index,
|
|
58
|
+
tagName: el.tagName.toLowerCase(),
|
|
59
|
+
id: el.id || null,
|
|
60
|
+
src: el.currentSrc || el.src,
|
|
61
|
+
state: {
|
|
62
|
+
paused: el.paused,
|
|
63
|
+
muted: el.muted,
|
|
64
|
+
ended: el.ended,
|
|
65
|
+
loop: el.loop,
|
|
66
|
+
playbackRate: el.playbackRate,
|
|
67
|
+
volume: el.volume
|
|
68
|
+
},
|
|
69
|
+
timing: {
|
|
70
|
+
currentTime: el.currentTime,
|
|
71
|
+
duration: el.duration
|
|
72
|
+
},
|
|
73
|
+
buffer: {
|
|
74
|
+
readyState: el.readyState,
|
|
75
|
+
buffered
|
|
76
|
+
},
|
|
77
|
+
videoSpecs: el.tagName === 'VIDEO' ? {
|
|
78
|
+
videoWidth: el.videoWidth,
|
|
79
|
+
videoHeight: el.videoHeight
|
|
80
|
+
} : undefined
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
content: [{
|
|
86
|
+
type: 'text',
|
|
87
|
+
text: JSON.stringify(mediaState, null, 2)
|
|
88
|
+
}]
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
browser_get_audio_analysis: async (args) => {
|
|
93
|
+
const { page } = await connectToBrowser();
|
|
94
|
+
const duration = args.durationMs || 2000;
|
|
95
|
+
const selector = args.selector;
|
|
96
|
+
|
|
97
|
+
const analysis = await page.evaluate(async ({ duration, selector }) => {
|
|
98
|
+
return new Promise(async (resolve) => {
|
|
99
|
+
try {
|
|
100
|
+
// 1. Find element
|
|
101
|
+
let element;
|
|
102
|
+
if (selector) {
|
|
103
|
+
element = document.querySelector(selector);
|
|
104
|
+
} else {
|
|
105
|
+
// Pick the first one playing or just the first one
|
|
106
|
+
const all = Array.from(document.querySelectorAll('audio, video'));
|
|
107
|
+
element = all.find(e => !e.paused) || all[0];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (!element) return resolve({ error: 'No media element found' });
|
|
111
|
+
|
|
112
|
+
// 2. Setup AudioContext (handle browser policies)
|
|
113
|
+
const CtxClass = window.AudioContext || window.webkitAudioContext;
|
|
114
|
+
if (!CtxClass) return resolve({ error: 'Web Audio API not supported' });
|
|
115
|
+
|
|
116
|
+
const ctx = new CtxClass();
|
|
117
|
+
|
|
118
|
+
// Resume context if suspended (common in browsers)
|
|
119
|
+
if (ctx.state === 'suspended') await ctx.resume();
|
|
120
|
+
|
|
121
|
+
// 3. Create Source & Analyzer
|
|
122
|
+
// Note: MediaElementSource requires the element to allow CORS if cross-origin
|
|
123
|
+
let source;
|
|
124
|
+
try {
|
|
125
|
+
source = ctx.createMediaElementSource(element);
|
|
126
|
+
} catch (e) {
|
|
127
|
+
// If already connected or tainted, this might fail.
|
|
128
|
+
// We can try to reconnect or just capture current data if available.
|
|
129
|
+
return resolve({ error: `Cannot connect to media source: ${e.message}. (Check CORS headers)` });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const analyzer = ctx.createAnalyser();
|
|
133
|
+
analyzer.fftSize = 256;
|
|
134
|
+
const bufferLength = analyzer.frequencyBinCount;
|
|
135
|
+
const dataArray = new Uint8Array(bufferLength);
|
|
136
|
+
|
|
137
|
+
source.connect(analyzer);
|
|
138
|
+
analyzer.connect(ctx.destination);
|
|
139
|
+
|
|
140
|
+
// 4. Collect samples over duration
|
|
141
|
+
const samples = [];
|
|
142
|
+
const startTime = Date.now();
|
|
143
|
+
const interval = setInterval(() => {
|
|
144
|
+
analyzer.getByteFrequencyData(dataArray);
|
|
145
|
+
|
|
146
|
+
// Calculate instant stats
|
|
147
|
+
let sum = 0;
|
|
148
|
+
let max = 0;
|
|
149
|
+
for (let i = 0; i < bufferLength; i++) {
|
|
150
|
+
const val = dataArray[i];
|
|
151
|
+
sum += val;
|
|
152
|
+
if (val > max) max = val;
|
|
153
|
+
}
|
|
154
|
+
const avg = sum / bufferLength;
|
|
155
|
+
|
|
156
|
+
samples.push({ avg, max, data: Array.from(dataArray) });
|
|
157
|
+
|
|
158
|
+
if (Date.now() - startTime >= duration) {
|
|
159
|
+
clearInterval(interval);
|
|
160
|
+
finalize();
|
|
161
|
+
}
|
|
162
|
+
}, 100); // 10 samples per second
|
|
163
|
+
|
|
164
|
+
function finalize() {
|
|
165
|
+
// Clean up
|
|
166
|
+
try {
|
|
167
|
+
source.disconnect();
|
|
168
|
+
analyzer.disconnect();
|
|
169
|
+
ctx.close();
|
|
170
|
+
} catch (e) { }
|
|
171
|
+
|
|
172
|
+
// Aggregate
|
|
173
|
+
if (samples.length === 0) return resolve({ status: 'No samples' });
|
|
174
|
+
|
|
175
|
+
const totalAvg = samples.reduce((a, b) => a + b.avg, 0) / samples.length;
|
|
176
|
+
const grandMax = Math.max(...samples.map(s => s.max));
|
|
177
|
+
const isSilent = grandMax < 5; // Low threshold
|
|
178
|
+
|
|
179
|
+
// Bucket frequencies (Simple approximation)
|
|
180
|
+
// 128 bins over Nyquist (e.g. 24kHz).
|
|
181
|
+
// Bass (0-4), Mid (5-40), Treble (41-127) roughly
|
|
182
|
+
const bassSum = samples.reduce((acc, s) => {
|
|
183
|
+
return acc + s.data.slice(0, 5).reduce((a, b) => a + b, 0) / 5;
|
|
184
|
+
}, 0) / samples.length;
|
|
185
|
+
|
|
186
|
+
const midSum = samples.reduce((acc, s) => {
|
|
187
|
+
return acc + s.data.slice(5, 40).reduce((a, b) => a + b, 0) / 35;
|
|
188
|
+
}, 0) / samples.length;
|
|
189
|
+
|
|
190
|
+
const trebleSum = samples.reduce((acc, s) => {
|
|
191
|
+
return acc + s.data.slice(40).reduce((a, b) => a + b, 0) / 88;
|
|
192
|
+
}, 0) / samples.length;
|
|
193
|
+
|
|
194
|
+
const activeFrequencies = [];
|
|
195
|
+
if (bassSum > 20) activeFrequencies.push('bass');
|
|
196
|
+
if (midSum > 20) activeFrequencies.push('mid');
|
|
197
|
+
if (trebleSum > 20) activeFrequencies.push('treble');
|
|
198
|
+
|
|
199
|
+
resolve({
|
|
200
|
+
element: { tagName: element.tagName, id: element.id, src: element.currentSrc },
|
|
201
|
+
isSilent,
|
|
202
|
+
averageVolume: Math.round(totalAvg),
|
|
203
|
+
peakVolume: grandMax,
|
|
204
|
+
activeFrequencies
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
} catch (e) {
|
|
209
|
+
resolve({ error: e.message });
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
}, { duration, selector });
|
|
213
|
+
|
|
214
|
+
return {
|
|
215
|
+
content: [{
|
|
216
|
+
type: 'text',
|
|
217
|
+
text: JSON.stringify(analysis, null, 2)
|
|
218
|
+
}]
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
browser_control_media: async (args) => {
|
|
223
|
+
const { page } = await connectToBrowser();
|
|
224
|
+
const controlResult = await page.evaluate(async ({ selector, action, value }) => {
|
|
225
|
+
const el = document.querySelector(selector);
|
|
226
|
+
if (!el) return { error: `Element not found: ${selector}` };
|
|
227
|
+
if (!(el instanceof HTMLMediaElement)) return { error: 'Element is not audio/video' };
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
switch (action) {
|
|
231
|
+
case 'play':
|
|
232
|
+
await el.play();
|
|
233
|
+
return { status: 'playing' };
|
|
234
|
+
case 'pause':
|
|
235
|
+
el.pause();
|
|
236
|
+
return { status: 'paused' };
|
|
237
|
+
case 'mute':
|
|
238
|
+
el.muted = true;
|
|
239
|
+
return { status: 'muted' };
|
|
240
|
+
case 'unmute':
|
|
241
|
+
el.muted = false;
|
|
242
|
+
return { status: 'unmuted' };
|
|
243
|
+
case 'seek':
|
|
244
|
+
if (typeof value !== 'number') return { error: 'Seek value required' };
|
|
245
|
+
el.currentTime = value;
|
|
246
|
+
return { status: 'seeked', newTime: el.currentTime };
|
|
247
|
+
default:
|
|
248
|
+
return { error: `Unknown media action: ${action}` };
|
|
249
|
+
}
|
|
250
|
+
} catch (e) {
|
|
251
|
+
return { error: e.message };
|
|
252
|
+
}
|
|
253
|
+
}, args);
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
content: [{
|
|
257
|
+
type: 'text',
|
|
258
|
+
text: JSON.stringify(controlResult, null, 2)
|
|
259
|
+
}]
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
module.exports = { definitions, handlers };
|