我编写了一个 NodeJS 应用程序,它执行以下操作:
http.createServer
函数提供一些静态文件的服务器我遇到的问题是 - 一切都很顺利,直到相机应该打开的步骤。按钮点击事件被很好地触发。但执行会在
await navigator.mediaDevices.getUserMedia()
调用时停止。至少在浏览器中打开的网页上完成一些手动用户交互后,相机才会打开。因此,即使我点击页面正文的任何部分,它也会恢复其余的 JS 执行(即打开相机并执行面部识别步骤)并成功完成该过程。
我给出了我的index.js文件的代码,其中包含NodeJS代码和index.html文件,其中包含页面布局和face-api.js人脸识别所需的必要JS代码。有人可以告诉我为什么浏览器在打开相机之前期望与网页进行一些用户交互吗?这似乎是安全功能,但我想知道有没有办法使用 Puppeteer 绕过它?我的最终目标是在运行 NodeJS 程序时实现无头并且根本不显示 Chrome 窗口。
index.js 代码
const puppeteer = require('puppeteer');
const http = require('http');
const fs = require('fs');
require('dotenv').config();
const getViewUrl = (url) => {
url = url == '/' ? 'index.html' : url;
url = url.indexOf('/') === 0 ? url.substring(1) : url;
return `public/${url}`;
};
const getContentType = (url) => {
if (url.endsWith('.js')) {
return 'text/javascript';
} else if (url.endsWith('.json')) {
return 'application/json';
} else if (url.endsWith('.html')) {
return 'text/html';
}
return 'application/octet-stream';
}
var server = null;
const PORT = process.env.PORT || 55193;
function startServer() {
server = http.createServer((request, response) => {
let viewUrl = getViewUrl(request.url);
fs.readFile(viewUrl, (error, data) => {
if (error) {
response.writeHead(404);
response.write("<h1>File Not Found</h1>")
} else {
response.writeHead(200, {
'Content-type': getContentType(viewUrl)
});
response.write(data);
}
response.end();
})
});
server.listen(PORT);
}
(async() => {
startServer();
// Launch the browser and open a new blank page
const browser = await puppeteer.launch({
headless: false,
dumpio: true,
args: ['--no-sandbox',
'--use-file-for-fake-video-capture=C:/Users/adm/Downloads/test.mjpeg'
]
});
var context = browser.defaultBrowserContext();
context.clearPermissionOverrides();
await context.overridePermissions("http://localhost:" + PORT + "/", ['camera', 'microphone']);
const page = await context.newPage();
page
.on('console', message =>
console.log(`${message.type().substr(0, 3).toUpperCase()} ${message.text()}`))
.on('pageerror', ({ message }) => console.log(message))
.on('response', response =>
console.log(`${response.status()} ${response.url()}`))
.on('requestfailed', request =>
console.log(`${request.failure().errorText} ${request.url()}`));
// Navigate the page to a URL
await page.goto('http://localhost:' + PORT + '/', { waitUntil: 'load' });
// Set screen size
await page.setViewport({ width: 1080, height: 1024 });
// Wait and click on first result
const searchResultSelector = await page.waitForSelector('#runBtn');
await page.click('#runBtn');
// Locate the full title with a unique string
const textSelector = await page.waitForSelector('.positionDiv');
const fullTitle = await textSelector.evaluate(el => el.textContent);
await browser.close();
console.log(fullTitle);
server.close();
})();
index.html代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
<meta name="author" content="Prithwiraj Bose <sribasu.com>" />
<title>FaceAPI</title>
<script src="js/face-api.min.js" type="text/javascript"></script>
</head>
<body>
<div id="content">
<div id="myDiv01">...</div><br>
<input type="button" value="run" id="runBtn" onclick="javascript: run();"><br><br>
<video onplay="onPlay(this)" id="inputVideo" autoplay muted width="640" height="480" style=" border: 1px solid #ddd;"></video><br>
<canvas id="overlay" width="640" height="480" style="position:relative; top:-487px; border: 1px solid #ddd;"></canvas><br>
</div>
<!-- Core theme JS-->
<script type="text/javascript">
function resizeCanvasAndResults(dimensions, canvas, results) {
const {
width,
height
} = dimensions instanceof HTMLVideoElement
?
faceapi.getMediaDimensions(dimensions) :
dimensions
canvas.width = width
canvas.height = height
return results
}
async function onPlay() {
const videoEl = document.getElementById('inputVideo')
const options = new faceapi.TinyFaceDetectorOptions({
inputSize: 128,
scoreThreshold: 0.3
})
result = await faceapi.detectSingleFace(videoEl, options).withFaceLandmarks(true)
if (result) {
var nose = result.landmarks.getNose();
var x = nose[3]._x;
document.getElementById('myDiv01').innerHTML = x > (640 / 2) ? (x > 500 ? 'Extreme Right' : 'Right') : (x < 100 ? 'Extreme Left' : 'Left');
document.getElementById('myDiv01').classList.add('positionDiv');
// Just printing the first of 68 face landmark x and y
}
setTimeout(() => onPlay())
}
async function run() {
await faceapi.loadTinyFaceDetectorModel('models/')
await faceapi.loadFaceLandmarkTinyModel('models/')
console.log("Step 1");
const stream = await navigator.mediaDevices.getUserMedia({
audio: false,
video: true
})
console.log("Step 2");
const videoEl = document.getElementById('inputVideo')
videoEl.srcObject = stream
}
</script>
</body>
</html>
NodeJS 控制台输出(直到手动页面被单击)
C:\Program Files\nodejs\node.exe .\index.js
200 http://localhost:8080/
index.js:67
200 http://localhost:8080/js/face-api.min.js
index.js:67
200 http://localhost:8080/favicon.ico
index.js:67
200 http://localhost:8080/models/tiny_face_detector_model-weights_manifest.json
index.js:67
200 http://localhost:8080/models/tiny_face_detector_model-shard1
index.js:67
200 http://localhost:8080/models/face_landmark_68_tiny_model-weights_manifest.json
index.js:67
200 http://localhost:8080/models/face_landmark_68_tiny_model-shard1
index.js:67
LOG Step 1
NodeJS 控制台输出(手动单击页面后)
LOG Step 2
index.js:64
Right
我尝试使用 puppeteer 强制模拟页面主体上的点击。但只有在网页上发生真正的人机交互之前,一切都不起作用!
我终于成功了。是的,我了解到,由于 Chrome 的安全功能,在没有任何真正的人工干预的情况下,真正的 UI 无法以编程方式进行交互。因此 Chrome 支持一个参数,可以将其传递给 Puppetter。这就是所谓的
--use-fake-ui-for-media-stream
。所以我的浏览器启动代码现在看起来像这样。我的问题中给出的原始代码中的其他所有内容都按预期工作。
const browser = await puppeteer.launch({
headless: "new",
args: [
'--no-sandbox',
'--use-fake-ui-for-media-stream'
]
});