|
|
<!DOCTYPE html>
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
<meta charset="UTF-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>📱 百度浏览器摄像头测试</title>
|
|
|
<style>
|
|
|
body {
|
|
|
font-family: Arial, sans-serif;
|
|
|
background: linear-gradient(135deg, #1e88e5 0%, #1565c0 100%);
|
|
|
color: white;
|
|
|
padding: 15px;
|
|
|
}
|
|
|
|
|
|
.btn {
|
|
|
background: #4CAF50;
|
|
|
color: white;
|
|
|
padding: 12px 20px;
|
|
|
border: none;
|
|
|
border-radius: 6px;
|
|
|
margin: 5px;
|
|
|
cursor: pointer;
|
|
|
text-decoration: none;
|
|
|
display: inline-block;
|
|
|
}
|
|
|
|
|
|
.btn:hover {
|
|
|
background: #45a049;
|
|
|
}
|
|
|
|
|
|
.btn-blue {
|
|
|
background: #2196F3;
|
|
|
}
|
|
|
|
|
|
.log-area {
|
|
|
background: rgba(0, 0, 0, 0.5);
|
|
|
padding: 15px;
|
|
|
height: 300px;
|
|
|
overflow-y: auto;
|
|
|
font-family: monospace;
|
|
|
font-size: 12px;
|
|
|
border-radius: 8px;
|
|
|
margin: 15px 0;
|
|
|
}
|
|
|
|
|
|
#testVideo {
|
|
|
width: 100%;
|
|
|
max-width: 400px;
|
|
|
height: auto;
|
|
|
border-radius: 5px;
|
|
|
}
|
|
|
|
|
|
.video-container {
|
|
|
background: #000;
|
|
|
border-radius: 8px;
|
|
|
padding: 10px;
|
|
|
margin: 15px 0;
|
|
|
text-align: center;
|
|
|
}
|
|
|
</style>
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
<h1>📱 百度浏览器摄像头测试</h1>
|
|
|
<p>专门针对百度浏览器的摄像头API兼容性测试</p>
|
|
|
|
|
|
<button class="btn" onclick="testBaiduCamera()">测试百度浏览器摄像头</button>
|
|
|
<button class="btn" onclick="testStandardAPI()">测试标准API</button>
|
|
|
<button class="btn" onclick="tryInputFileCapture()" style="background: #FF9800;">📷 尝试文件捕获</button>
|
|
|
<button class="btn" onclick="initCustomBottomCamera()" style="background: #FF5722; font-weight: bold;">🔧
|
|
|
自定义摄像头</button>
|
|
|
<button class="btn" onclick="initBottomLevelCamera()" style="background: #4CAF50; font-weight: bold;">⚡
|
|
|
底层摄像头</button>
|
|
|
<button class="btn" onclick="startSimpleWebCamera()">🎥 简单摄像头</button>
|
|
|
<button class="btn" onclick="tryRealTimeCapture()">📹 实时视频流</button>
|
|
|
<button class="btn" onclick="detectAllAPIs()">🔍 深度API检测</button>
|
|
|
<button class="btn" onclick="clearLog()">清空日志</button>
|
|
|
|
|
|
<div class="video-container">
|
|
|
<video id="testVideo" autoplay muted playsinline style="display: none;"></video>
|
|
|
<div id="videoPlaceholder">点击按钮测试摄像头</div>
|
|
|
</div>
|
|
|
|
|
|
<div id="logArea" class="log-area"></div>
|
|
|
|
|
|
<div style="text-align: center; margin: 20px 0;">
|
|
|
<a href="mobile_client.html" class="btn btn-blue">🚁 返回移动终端</a>
|
|
|
</div>
|
|
|
|
|
|
<script>
|
|
|
let currentStream = null;
|
|
|
|
|
|
function log(message) {
|
|
|
const logArea = document.getElementById('logArea');
|
|
|
const timestamp = new Date().toLocaleTimeString();
|
|
|
logArea.innerHTML += `${timestamp} - ${message}<br>`;
|
|
|
logArea.scrollTop = logArea.scrollHeight;
|
|
|
console.log(message);
|
|
|
}
|
|
|
|
|
|
function clearLog() {
|
|
|
document.getElementById('logArea').innerHTML = '';
|
|
|
}
|
|
|
|
|
|
function showVideo(stream) {
|
|
|
const video = document.getElementById('testVideo');
|
|
|
video.srcObject = stream;
|
|
|
video.style.display = 'block';
|
|
|
document.getElementById('videoPlaceholder').style.display = 'none';
|
|
|
currentStream = stream;
|
|
|
log('✅ 摄像头启动成功!');
|
|
|
}
|
|
|
|
|
|
function testBaiduCamera() {
|
|
|
log('🔍 测试百度浏览器专用方法...');
|
|
|
|
|
|
const constraints = {
|
|
|
video: { facingMode: 'environment' },
|
|
|
audio: false
|
|
|
};
|
|
|
|
|
|
// 方法1: 检查百度浏览器特殊对象
|
|
|
if (window.external) {
|
|
|
log('📱 发现window.external对象,尝试百度专用API...');
|
|
|
|
|
|
// 尝试百度浏览器的专用方法
|
|
|
const baiduMethods = [
|
|
|
'GetUserMedia',
|
|
|
'getUserMedia',
|
|
|
'requestUserMedia',
|
|
|
'getCameraAccess',
|
|
|
'openCamera'
|
|
|
];
|
|
|
|
|
|
for (let method of baiduMethods) {
|
|
|
if (window.external[method]) {
|
|
|
log(`发现百度方法: external.${method}`);
|
|
|
try {
|
|
|
if (method === 'GetUserMedia') {
|
|
|
window.external.GetUserMedia(JSON.stringify(constraints), showVideo, (err) => {
|
|
|
log(`百度${method}失败: ${err}`);
|
|
|
});
|
|
|
return;
|
|
|
} else {
|
|
|
window.external[method](constraints, showVideo, (err) => {
|
|
|
log(`百度${method}失败: ${err.message || err}`);
|
|
|
});
|
|
|
return;
|
|
|
}
|
|
|
} catch (e) {
|
|
|
log(`百度${method}调用异常: ${e.message}`);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 方法2: 尝试标准但可能隐藏的webkitGetUserMedia
|
|
|
if (navigator.webkitGetUserMedia) {
|
|
|
log('📱 发现webkitGetUserMedia,尝试调用...');
|
|
|
navigator.webkitGetUserMedia(constraints, showVideo, (err) => {
|
|
|
log('❌ webkitGetUserMedia失败: ' + err.message);
|
|
|
tryAdvancedMethods();
|
|
|
});
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
tryAdvancedMethods();
|
|
|
}
|
|
|
|
|
|
function tryAdvancedMethods() {
|
|
|
log('🔍 尝试高级检测方法...');
|
|
|
|
|
|
// 方法1: 检查所有可能的全局对象
|
|
|
const globalObjects = [
|
|
|
'BaiduBrowser',
|
|
|
'baiduBrowser',
|
|
|
'BAIDU_BROWSER',
|
|
|
'BDBrowser',
|
|
|
'bdBrowser',
|
|
|
'UcBrowser',
|
|
|
'ucBrowser',
|
|
|
'QQBrowser',
|
|
|
'qqBrowser'
|
|
|
];
|
|
|
|
|
|
for (let objName of globalObjects) {
|
|
|
if (window[objName]) {
|
|
|
log(`🎯 发现全局对象: ${objName}`);
|
|
|
|
|
|
const obj = window[objName];
|
|
|
for (let prop in obj) {
|
|
|
if (prop.toLowerCase().includes('camera') ||
|
|
|
prop.toLowerCase().includes('media') ||
|
|
|
prop.toLowerCase().includes('getusermedia')) {
|
|
|
log(` - 发现方法: ${objName}.${prop}`);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 尝试常见的方法名
|
|
|
const methods = ['getUserMedia', 'getCamera', 'requestCamera', 'openCamera'];
|
|
|
for (let method of methods) {
|
|
|
if (obj[method] && typeof obj[method] === 'function') {
|
|
|
log(`🚀 尝试调用: ${objName}.${method}`);
|
|
|
try {
|
|
|
obj[method]({
|
|
|
video: { facingMode: 'environment' },
|
|
|
audio: false
|
|
|
}, showVideo, (err) => {
|
|
|
log(`❌ ${objName}.${method}失败: ${err.message || err}`);
|
|
|
});
|
|
|
return;
|
|
|
} catch (e) {
|
|
|
log(`❌ ${objName}.${method}异常: ${e.message}`);
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 方法2: 检查document对象的特殊方法
|
|
|
log('🔍 检查document对象...');
|
|
|
if (document.getCameraAccess) {
|
|
|
log('发现document.getCameraAccess');
|
|
|
try {
|
|
|
document.getCameraAccess(showVideo, (err) => {
|
|
|
log('document.getCameraAccess失败: ' + err);
|
|
|
});
|
|
|
return;
|
|
|
} catch (e) {
|
|
|
log('document.getCameraAccess异常: ' + e.message);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 方法3: 尝试创建video元素并设置src为摄像头
|
|
|
log('🔍 尝试直接设置video元素...');
|
|
|
const video = document.getElementById('testVideo');
|
|
|
try {
|
|
|
// 某些浏览器支持直接设置src为摄像头设备
|
|
|
video.src = 'camera:';
|
|
|
video.play();
|
|
|
log('尝试camera:协议');
|
|
|
|
|
|
setTimeout(() => {
|
|
|
if (video.videoWidth > 0) {
|
|
|
log('✅ camera:协议成功!');
|
|
|
video.style.display = 'block';
|
|
|
document.getElementById('videoPlaceholder').style.display = 'none';
|
|
|
}
|
|
|
}, 2000);
|
|
|
} catch (e) {
|
|
|
log('camera:协议失败: ' + e.message);
|
|
|
}
|
|
|
|
|
|
// 方法4: 使用input file + capture
|
|
|
log('🔍 尝试input file capture方法...');
|
|
|
tryInputFileCapture();
|
|
|
}
|
|
|
|
|
|
function tryInputFileCapture() {
|
|
|
log('📷 创建文件输入捕获...');
|
|
|
|
|
|
const input = document.createElement('input');
|
|
|
input.type = 'file';
|
|
|
input.accept = 'image/*,video/*';
|
|
|
input.capture = 'camera';
|
|
|
input.style.position = 'absolute';
|
|
|
input.style.top = '-1000px';
|
|
|
document.body.appendChild(input);
|
|
|
|
|
|
input.onchange = function (e) {
|
|
|
const file = e.target.files[0];
|
|
|
if (file) {
|
|
|
log('✅ 用户选择了文件: ' + file.name);
|
|
|
|
|
|
if (file.type.startsWith('image/')) {
|
|
|
const url = URL.createObjectURL(file);
|
|
|
const img = document.createElement('img');
|
|
|
img.src = url;
|
|
|
img.style.maxWidth = '100%';
|
|
|
img.style.maxHeight = '100%';
|
|
|
|
|
|
const container = document.querySelector('.video-container');
|
|
|
container.innerHTML = '';
|
|
|
container.appendChild(img);
|
|
|
|
|
|
log('✅ 图片显示成功');
|
|
|
} else if (file.type.startsWith('video/')) {
|
|
|
const url = URL.createObjectURL(file);
|
|
|
const video = document.getElementById('testVideo');
|
|
|
video.src = url;
|
|
|
video.style.display = 'block';
|
|
|
document.getElementById('videoPlaceholder').style.display = 'none';
|
|
|
log('✅ 视频显示成功');
|
|
|
}
|
|
|
}
|
|
|
document.body.removeChild(input);
|
|
|
};
|
|
|
|
|
|
input.onerror = function () {
|
|
|
log('❌ 文件输入失败');
|
|
|
document.body.removeChild(input);
|
|
|
};
|
|
|
|
|
|
// 自动触发文件选择
|
|
|
setTimeout(() => {
|
|
|
input.click();
|
|
|
log('📱 已触发摄像头文件选择(某些浏览器会直接打开摄像头)');
|
|
|
}, 1000);
|
|
|
}
|
|
|
|
|
|
// 实时视频流捕获
|
|
|
function tryRealTimeCapture() {
|
|
|
log('📹 尝试启动真正的实时视频流...', 'info');
|
|
|
|
|
|
// 首先尝试标准方法
|
|
|
if (tryStandardVideoStream()) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 如果标准方法失败,尝试百度浏览器专用方法
|
|
|
if (tryBaiduVideoStream()) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 最后尝试模拟实时流
|
|
|
log('🔄 启动模拟实时视频流...', 'info');
|
|
|
startSimulatedVideoStream();
|
|
|
}
|
|
|
|
|
|
function tryStandardVideoStream() {
|
|
|
if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
|
|
|
log('❌ 标准视频流API不可用', 'warning');
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
log('🎯 尝试标准getUserMedia...', 'info');
|
|
|
const constraints = {
|
|
|
video: {
|
|
|
facingMode: 'environment',
|
|
|
width: { ideal: 640 },
|
|
|
height: { ideal: 480 }
|
|
|
},
|
|
|
audio: false
|
|
|
};
|
|
|
|
|
|
navigator.mediaDevices.getUserMedia(constraints)
|
|
|
.then(stream => {
|
|
|
log('✅ 标准视频流获取成功!', 'success');
|
|
|
setupVideoStream(stream);
|
|
|
})
|
|
|
.catch(error => {
|
|
|
log(`❌ 标准视频流失败: ${error.message}`, 'error');
|
|
|
});
|
|
|
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
function tryBaiduVideoStream() {
|
|
|
if (!window.external) {
|
|
|
log('❌ window.external不可用', 'warning');
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
log('🎯 尝试百度浏览器视频流...', 'info');
|
|
|
const methods = ['GetUserMedia', 'getUserMedia', 'startCamera'];
|
|
|
|
|
|
for (let method of methods) {
|
|
|
if (window.external[method]) {
|
|
|
log(`发现external.${method},尝试获取视频流...`, 'info');
|
|
|
try {
|
|
|
const constraints = {
|
|
|
video: { facingMode: 'environment' },
|
|
|
audio: false
|
|
|
};
|
|
|
|
|
|
if (method === 'GetUserMedia') {
|
|
|
window.external.GetUserMedia(
|
|
|
JSON.stringify(constraints),
|
|
|
(stream) => {
|
|
|
log('✅ 百度视频流获取成功!', 'success');
|
|
|
setupVideoStream(stream);
|
|
|
},
|
|
|
(error) => {
|
|
|
log(`百度${method}失败: ${error}`, 'warning');
|
|
|
}
|
|
|
);
|
|
|
} else {
|
|
|
window.external[method](
|
|
|
constraints,
|
|
|
(stream) => {
|
|
|
log('✅ 百度视频流获取成功!', 'success');
|
|
|
setupVideoStream(stream);
|
|
|
},
|
|
|
(error) => {
|
|
|
log(`百度${method}失败: ${error.message || error}`, 'warning');
|
|
|
}
|
|
|
);
|
|
|
}
|
|
|
return true;
|
|
|
} catch (e) {
|
|
|
log(`百度${method}调用异常: ${e.message}`, 'warning');
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
function setupVideoStream(stream) {
|
|
|
const video = document.getElementById('testVideo');
|
|
|
|
|
|
try {
|
|
|
if (stream instanceof MediaStream) {
|
|
|
video.srcObject = stream;
|
|
|
} else {
|
|
|
video.src = URL.createObjectURL(stream);
|
|
|
}
|
|
|
|
|
|
video.style.display = 'block';
|
|
|
document.getElementById('videoPlaceholder').style.display = 'none';
|
|
|
currentStream = stream;
|
|
|
|
|
|
video.onloadedmetadata = () => {
|
|
|
log(`✅ 视频流已加载: ${video.videoWidth}x${video.videoHeight}`, 'success');
|
|
|
startFrameCapture(video);
|
|
|
};
|
|
|
|
|
|
video.play();
|
|
|
|
|
|
} catch (e) {
|
|
|
log(`❌ 视频流设置失败: ${e.message}`, 'error');
|
|
|
startSimulatedVideoStream();
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function startFrameCapture(video) {
|
|
|
log('🎬 开始实时帧捕获...', 'success');
|
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
let frameCount = 0;
|
|
|
|
|
|
const captureFrame = () => {
|
|
|
if (!currentStream) return;
|
|
|
|
|
|
canvas.width = video.videoWidth || 640;
|
|
|
canvas.height = video.videoHeight || 480;
|
|
|
|
|
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
frameCount++;
|
|
|
if (frameCount % 30 === 0) { // 每30帧输出一次日志
|
|
|
log(`📊 已捕获 ${frameCount} 帧`, 'info');
|
|
|
}
|
|
|
|
|
|
// 继续下一帧
|
|
|
requestAnimationFrame(captureFrame);
|
|
|
};
|
|
|
|
|
|
// 开始捕获
|
|
|
requestAnimationFrame(captureFrame);
|
|
|
}
|
|
|
|
|
|
function startSimulatedVideoStream() {
|
|
|
log('🎭 启动模拟实时视频流...', 'info');
|
|
|
log('💡 这将通过连续拍照来模拟视频流效果', 'info');
|
|
|
|
|
|
const video = document.getElementById('testVideo');
|
|
|
const canvas = document.createElement('canvas');
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
let captureCount = 0;
|
|
|
let isRunning = true;
|
|
|
|
|
|
// 创建停止按钮
|
|
|
const stopBtn = document.createElement('button');
|
|
|
stopBtn.textContent = '⏹️ 停止模拟流';
|
|
|
stopBtn.className = 'btn';
|
|
|
stopBtn.style.background = '#f44336';
|
|
|
stopBtn.style.margin = '10px';
|
|
|
stopBtn.onclick = () => {
|
|
|
isRunning = false;
|
|
|
stopBtn.remove();
|
|
|
log('🛑 模拟视频流已停止', 'info');
|
|
|
};
|
|
|
document.querySelector('.video-container').appendChild(stopBtn);
|
|
|
|
|
|
const captureNext = () => {
|
|
|
if (!isRunning) return;
|
|
|
|
|
|
const input = document.createElement('input');
|
|
|
input.type = 'file';
|
|
|
input.accept = 'image/*';
|
|
|
input.capture = 'camera';
|
|
|
input.style.position = 'absolute';
|
|
|
input.style.top = '-1000px';
|
|
|
document.body.appendChild(input);
|
|
|
|
|
|
input.onchange = (e) => {
|
|
|
const file = e.target.files[0];
|
|
|
if (file && file.type.startsWith('image/')) {
|
|
|
captureCount++;
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
reader.onload = (event) => {
|
|
|
const img = new Image();
|
|
|
img.onload = () => {
|
|
|
// 更新video元素显示
|
|
|
canvas.width = img.width;
|
|
|
canvas.height = img.height;
|
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
|
|
const dataURL = canvas.toDataURL('image/jpeg', 0.8);
|
|
|
video.style.backgroundImage = `url(${dataURL})`;
|
|
|
video.style.backgroundSize = 'contain';
|
|
|
video.style.backgroundRepeat = 'no-repeat';
|
|
|
video.style.backgroundPosition = 'center';
|
|
|
video.style.display = 'block';
|
|
|
document.getElementById('videoPlaceholder').style.display = 'none';
|
|
|
|
|
|
log(`📸 更新帧 ${captureCount}`, 'success');
|
|
|
|
|
|
// 快速连续拍摄
|
|
|
if (isRunning) {
|
|
|
setTimeout(captureNext, 800); // 0.8秒间隔
|
|
|
}
|
|
|
};
|
|
|
img.src = event.target.result;
|
|
|
};
|
|
|
reader.readAsDataURL(file);
|
|
|
}
|
|
|
document.body.removeChild(input);
|
|
|
};
|
|
|
|
|
|
input.onerror = () => {
|
|
|
log('❌ 拍照失败', 'error');
|
|
|
document.body.removeChild(input);
|
|
|
if (isRunning) {
|
|
|
setTimeout(captureNext, 2000); // 失败后等2秒重试
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 延迟一下再触发,避免过于频繁
|
|
|
setTimeout(() => {
|
|
|
if (isRunning) {
|
|
|
input.click();
|
|
|
}
|
|
|
}, 100);
|
|
|
};
|
|
|
|
|
|
log('📱 点击拍照来开始模拟视频流...', 'info');
|
|
|
captureNext();
|
|
|
}
|
|
|
|
|
|
function testStandardAPI() {
|
|
|
log('🔧 测试标准MediaDevices API...');
|
|
|
|
|
|
if (!navigator.mediaDevices) {
|
|
|
log('❌ navigator.mediaDevices 不存在');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (!navigator.mediaDevices.getUserMedia) {
|
|
|
log('❌ navigator.mediaDevices.getUserMedia 不存在');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
navigator.mediaDevices.getUserMedia({
|
|
|
video: { facingMode: 'environment' },
|
|
|
audio: false
|
|
|
}).then(showVideo).catch(err => {
|
|
|
log('❌ 标准API失败: ' + err.message);
|
|
|
});
|
|
|
}
|
|
|
|
|
|
// 深度API检测
|
|
|
function detectAllAPIs() {
|
|
|
log('🔍 开始深度API检测...');
|
|
|
|
|
|
// 检查navigator对象的所有属性
|
|
|
log('📋 Navigator对象属性:');
|
|
|
for (let prop in navigator) {
|
|
|
try {
|
|
|
if (prop.toLowerCase().includes('camera') ||
|
|
|
prop.toLowerCase().includes('media') ||
|
|
|
prop.toLowerCase().includes('getuser') ||
|
|
|
prop.toLowerCase().includes('device')) {
|
|
|
log(` - navigator.${prop}: ${typeof navigator[prop]}`);
|
|
|
}
|
|
|
} catch (e) { }
|
|
|
}
|
|
|
|
|
|
// 检查window对象的特殊属性
|
|
|
log('🌐 Window对象特殊属性:');
|
|
|
const specialProps = [
|
|
|
'external', 'BaiduBrowser', 'baiduBrowser', 'BAIDU_BROWSER',
|
|
|
'UcBrowser', 'ucBrowser', 'QQBrowser', 'qqBrowser',
|
|
|
'webkit', 'moz', 'ms', 'o'
|
|
|
];
|
|
|
|
|
|
for (let prop of specialProps) {
|
|
|
if (window[prop]) {
|
|
|
log(` - window.${prop}: ${typeof window[prop]}`);
|
|
|
|
|
|
if (typeof window[prop] === 'object') {
|
|
|
for (let subProp in window[prop]) {
|
|
|
try {
|
|
|
if (subProp.toLowerCase().includes('camera') ||
|
|
|
subProp.toLowerCase().includes('media') ||
|
|
|
subProp.toLowerCase().includes('getuser')) {
|
|
|
log(` * ${prop}.${subProp}: ${typeof window[prop][subProp]}`);
|
|
|
}
|
|
|
} catch (e) { }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 检查document对象
|
|
|
log('📄 Document对象方法:');
|
|
|
for (let prop in document) {
|
|
|
try {
|
|
|
if (prop.toLowerCase().includes('camera') ||
|
|
|
prop.toLowerCase().includes('media') ||
|
|
|
prop.toLowerCase().includes('getuser')) {
|
|
|
log(` - document.${prop}: ${typeof document[prop]}`);
|
|
|
}
|
|
|
} catch (e) { }
|
|
|
}
|
|
|
|
|
|
log('🔍 深度检测完成');
|
|
|
}
|
|
|
|
|
|
// 简单的Web摄像头接口
|
|
|
function startSimpleWebCamera() {
|
|
|
log('🎥 启动简单Web摄像头接口...', 'info');
|
|
|
|
|
|
// 尝试最基本的方法
|
|
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
|
log('📹 尝试基础getUserMedia...', 'info');
|
|
|
navigator.mediaDevices.getUserMedia({
|
|
|
video: { facingMode: 'environment' },
|
|
|
audio: false
|
|
|
})
|
|
|
.then(stream => {
|
|
|
log('✅ 摄像头启动成功!', 'success');
|
|
|
displaySimpleVideo(stream);
|
|
|
})
|
|
|
.catch(error => {
|
|
|
log(`摄像头失败: ${error.message}`, 'error');
|
|
|
startSimpleFileMode();
|
|
|
});
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 尝试老式API
|
|
|
const getUserMedia = navigator.getUserMedia ||
|
|
|
navigator.webkitGetUserMedia ||
|
|
|
navigator.mozGetUserMedia ||
|
|
|
navigator.msGetUserMedia;
|
|
|
|
|
|
if (getUserMedia) {
|
|
|
log('📹 尝试老式getUserMedia...', 'info');
|
|
|
getUserMedia.call(navigator, {
|
|
|
video: { facingMode: 'environment' },
|
|
|
audio: false
|
|
|
},
|
|
|
stream => {
|
|
|
log('✅ 老式API成功!', 'success');
|
|
|
displaySimpleVideo(stream);
|
|
|
},
|
|
|
error => {
|
|
|
log(`老式API失败: ${error.message}`, 'error');
|
|
|
startSimpleFileMode();
|
|
|
});
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
log('❌ 无摄像头API可用,启动文件模式', 'warning');
|
|
|
startSimpleFileMode();
|
|
|
}
|
|
|
|
|
|
function displaySimpleVideo(stream) {
|
|
|
const video = document.getElementById('testVideo');
|
|
|
video.srcObject = stream;
|
|
|
video.style.display = 'block';
|
|
|
video.play();
|
|
|
|
|
|
document.getElementById('videoPlaceholder').style.display = 'none';
|
|
|
currentStream = stream;
|
|
|
|
|
|
log('🎬 实时视频已开始显示', 'success');
|
|
|
|
|
|
// 添加停止按钮
|
|
|
const stopBtn = document.createElement('button');
|
|
|
stopBtn.textContent = '⏹️ 停止摄像头';
|
|
|
stopBtn.className = 'btn';
|
|
|
stopBtn.style.background = '#f44336';
|
|
|
stopBtn.style.margin = '10px';
|
|
|
stopBtn.onclick = () => {
|
|
|
if (currentStream) {
|
|
|
currentStream.getTracks().forEach(track => track.stop());
|
|
|
currentStream = null;
|
|
|
}
|
|
|
video.srcObject = null;
|
|
|
video.style.display = 'none';
|
|
|
document.getElementById('videoPlaceholder').style.display = 'block';
|
|
|
stopBtn.remove();
|
|
|
log('🛑 摄像头已停止', 'info');
|
|
|
};
|
|
|
document.querySelector('.video-container').appendChild(stopBtn);
|
|
|
}
|
|
|
|
|
|
function startSimpleFileMode() {
|
|
|
log('📷 启动简单文件拍照模式...', 'info');
|
|
|
|
|
|
// 创建拍照按钮
|
|
|
const photoBtn = document.createElement('button');
|
|
|
photoBtn.textContent = '📸 拍照';
|
|
|
photoBtn.className = 'btn';
|
|
|
photoBtn.style.background = '#2196F3';
|
|
|
photoBtn.style.margin = '10px';
|
|
|
photoBtn.style.fontSize = '16px';
|
|
|
photoBtn.style.padding = '12px 20px';
|
|
|
|
|
|
let photoCount = 0;
|
|
|
|
|
|
photoBtn.onclick = () => {
|
|
|
const input = document.createElement('input');
|
|
|
input.type = 'file';
|
|
|
input.accept = 'image/*';
|
|
|
input.capture = 'camera';
|
|
|
input.style.display = 'none';
|
|
|
|
|
|
input.onchange = (e) => {
|
|
|
const file = e.target.files[0];
|
|
|
if (file) {
|
|
|
photoCount++;
|
|
|
log(`📸 拍照 ${photoCount}: ${file.name}`, 'success');
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
reader.onload = (event) => {
|
|
|
const video = document.getElementById('testVideo');
|
|
|
video.style.backgroundImage = `url(${event.target.result})`;
|
|
|
video.style.backgroundSize = 'contain';
|
|
|
video.style.backgroundRepeat = 'no-repeat';
|
|
|
video.style.backgroundPosition = 'center';
|
|
|
video.style.display = 'block';
|
|
|
document.getElementById('videoPlaceholder').style.display = 'none';
|
|
|
|
|
|
log('✅ 照片已显示', 'success');
|
|
|
};
|
|
|
reader.readAsDataURL(file);
|
|
|
}
|
|
|
document.body.removeChild(input);
|
|
|
};
|
|
|
|
|
|
document.body.appendChild(input);
|
|
|
input.click();
|
|
|
};
|
|
|
|
|
|
document.querySelector('.video-container').appendChild(photoBtn);
|
|
|
log('📱 简单拍照模式已启用,点击"📸 拍照"按钮开始', 'success');
|
|
|
}
|
|
|
|
|
|
window.onload = function () {
|
|
|
log('🚀 页面加载完成');
|
|
|
log('UserAgent: ' + navigator.userAgent);
|
|
|
log('Platform: ' + navigator.platform);
|
|
|
|
|
|
// 检查可用的API
|
|
|
log('📋 基础API检查:');
|
|
|
log('- navigator.mediaDevices: ' + (navigator.mediaDevices ? '✅' : '❌'));
|
|
|
log('- navigator.getUserMedia: ' + (navigator.getUserMedia ? '✅' : '❌'));
|
|
|
log('- navigator.webkitGetUserMedia: ' + (navigator.webkitGetUserMedia ? '✅' : '❌'));
|
|
|
log('- navigator.mozGetUserMedia: ' + (navigator.mozGetUserMedia ? '✅' : '❌'));
|
|
|
log('- window.external: ' + (window.external ? '✅' : '❌'));
|
|
|
|
|
|
// 自动进行深度检测
|
|
|
setTimeout(() => {
|
|
|
detectAllAPIs();
|
|
|
}, 1000);
|
|
|
};
|
|
|
|
|
|
window.onbeforeunload = function () {
|
|
|
if (currentStream) {
|
|
|
currentStream.getTracks().forEach(track => track.stop());
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 底层实时摄像头API for 百度浏览器
|
|
|
async function initBottomLevelCamera() {
|
|
|
log('🎥 初始化底层摄像头API...', 'info');
|
|
|
|
|
|
const constraints = {
|
|
|
video: {
|
|
|
facingMode: { ideal: 'environment' },
|
|
|
width: { min: 320, ideal: 640, max: 1280 },
|
|
|
height: { min: 240, ideal: 480, max: 720 },
|
|
|
frameRate: { min: 15, ideal: 30, max: 60 }
|
|
|
},
|
|
|
audio: false
|
|
|
};
|
|
|
|
|
|
try {
|
|
|
// 底层获取媒体流
|
|
|
const stream = await acquireRealTimeStream(constraints);
|
|
|
if (stream) {
|
|
|
setupBottomLevelVideo(stream);
|
|
|
startBottomLevelProcessing();
|
|
|
return true;
|
|
|
}
|
|
|
} catch (error) {
|
|
|
log(`底层摄像头API失败: ${error.message}`, 'error');
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
async function acquireRealTimeStream(constraints) {
|
|
|
log('📡 获取底层媒体流...', 'info');
|
|
|
|
|
|
// 方法1: 现代MediaDevices API
|
|
|
if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
|
|
|
try {
|
|
|
log('🔄 使用MediaDevices.getUserMedia()', 'info');
|
|
|
const stream = await navigator.mediaDevices.getUserMedia(constraints);
|
|
|
log('✅ MediaDevices API成功', 'success');
|
|
|
return stream;
|
|
|
} catch (error) {
|
|
|
log(`MediaDevices失败: ${error.name} - ${error.message}`, 'warning');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 方法2: 传统getUserMedia with Promises
|
|
|
const legacyGetUserMedia = navigator.getUserMedia ||
|
|
|
navigator.webkitGetUserMedia ||
|
|
|
navigator.mozGetUserMedia ||
|
|
|
navigator.msGetUserMedia;
|
|
|
|
|
|
if (legacyGetUserMedia) {
|
|
|
try {
|
|
|
log('🔄 使用传统getUserMedia', 'info');
|
|
|
return await new Promise((resolve, reject) => {
|
|
|
legacyGetUserMedia.call(navigator, constraints, resolve, reject);
|
|
|
});
|
|
|
} catch (error) {
|
|
|
log(`传统API失败: ${error.message}`, 'warning');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
throw new Error('无可用的媒体流API');
|
|
|
}
|
|
|
|
|
|
function setupBottomLevelVideo(stream) {
|
|
|
log('🎬 设置实时视频显示...', 'info');
|
|
|
|
|
|
const video = document.getElementById('testVideo');
|
|
|
|
|
|
// 设置视频元素
|
|
|
video.srcObject = stream;
|
|
|
video.style.display = 'block';
|
|
|
video.autoplay = true;
|
|
|
video.playsInline = true;
|
|
|
video.muted = true;
|
|
|
|
|
|
// 等待视频元数据加载
|
|
|
video.onloadedmetadata = () => {
|
|
|
log(`📺 视频流规格: ${video.videoWidth}x${video.videoHeight}`, 'success');
|
|
|
video.play();
|
|
|
};
|
|
|
|
|
|
document.getElementById('videoPlaceholder').style.display = 'none';
|
|
|
currentStream = stream;
|
|
|
isBottomLevelActive = true;
|
|
|
|
|
|
// 添加底层控制按钮
|
|
|
addBottomLevelControls();
|
|
|
|
|
|
log('✅ 底层实时视频流已启动', 'success');
|
|
|
}
|
|
|
|
|
|
function startBottomLevelProcessing() {
|
|
|
log('⚡ 启动底层帧处理引擎...', 'success');
|
|
|
|
|
|
let frameCount = 0;
|
|
|
let lastTime = performance.now();
|
|
|
let fps = 0;
|
|
|
|
|
|
const processFrame = (currentTime) => {
|
|
|
if (!isBottomLevelActive || !currentStream) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 计算FPS
|
|
|
const deltaTime = currentTime - lastTime;
|
|
|
if (deltaTime >= 1000) { // 每秒更新一次FPS
|
|
|
fps = Math.round((frameCount * 1000) / deltaTime);
|
|
|
log(`📊 底层处理: ${fps} FPS, 帧数: ${frameCount}`, 'info');
|
|
|
frameCount = 0;
|
|
|
lastTime = currentTime;
|
|
|
}
|
|
|
|
|
|
frameCount++;
|
|
|
|
|
|
// 请求下一帧
|
|
|
requestAnimationFrame(processFrame);
|
|
|
};
|
|
|
|
|
|
// 开始处理循环
|
|
|
requestAnimationFrame(processFrame);
|
|
|
}
|
|
|
|
|
|
function addBottomLevelControls() {
|
|
|
const container = document.querySelector('.video-container');
|
|
|
|
|
|
// 停止按钮
|
|
|
const stopBtn = document.createElement('button');
|
|
|
stopBtn.textContent = '⏹️ 停止底层摄像头';
|
|
|
stopBtn.className = 'btn';
|
|
|
stopBtn.style.background = '#f44336';
|
|
|
stopBtn.style.margin = '5px';
|
|
|
stopBtn.onclick = stopBottomLevelCamera;
|
|
|
|
|
|
// 信息按钮
|
|
|
const infoBtn = document.createElement('button');
|
|
|
infoBtn.textContent = '📋 摄像头信息';
|
|
|
infoBtn.className = 'btn';
|
|
|
infoBtn.style.background = '#9C27B0';
|
|
|
infoBtn.style.margin = '5px';
|
|
|
infoBtn.onclick = getBottomLevelCameraInfo;
|
|
|
|
|
|
// 质量调整按钮
|
|
|
const qualityBtn = document.createElement('button');
|
|
|
qualityBtn.textContent = '🎬 高质量模式';
|
|
|
qualityBtn.className = 'btn';
|
|
|
qualityBtn.style.background = '#FF5722';
|
|
|
qualityBtn.style.margin = '5px';
|
|
|
qualityBtn.onclick = () => adjustBottomLevelQuality(1280, 720, 60);
|
|
|
|
|
|
container.appendChild(stopBtn);
|
|
|
container.appendChild(infoBtn);
|
|
|
container.appendChild(qualityBtn);
|
|
|
}
|
|
|
|
|
|
function stopBottomLevelCamera() {
|
|
|
log('🛑 停止底层摄像头...', 'info');
|
|
|
|
|
|
isBottomLevelActive = false;
|
|
|
|
|
|
// 停止所有媒体轨道
|
|
|
if (currentStream) {
|
|
|
currentStream.getTracks().forEach(track => {
|
|
|
track.stop();
|
|
|
log(`⏹️ 停止轨道: ${track.kind} (${track.label || 'unknown'})`, 'info');
|
|
|
});
|
|
|
currentStream = null;
|
|
|
}
|
|
|
|
|
|
// 清理视频元素
|
|
|
const video = document.getElementById('testVideo');
|
|
|
video.srcObject = null;
|
|
|
video.style.display = 'none';
|
|
|
|
|
|
// 重置UI
|
|
|
document.getElementById('videoPlaceholder').style.display = 'block';
|
|
|
|
|
|
// 移除控制按钮
|
|
|
const container = document.querySelector('.video-container');
|
|
|
const buttons = container.querySelectorAll('.btn');
|
|
|
buttons.forEach(btn => {
|
|
|
if (btn.textContent.includes('停止底层') ||
|
|
|
btn.textContent.includes('摄像头信息') ||
|
|
|
btn.textContent.includes('高质量模式')) {
|
|
|
btn.remove();
|
|
|
}
|
|
|
});
|
|
|
|
|
|
log('✅ 底层摄像头已完全停止', 'success');
|
|
|
}
|
|
|
|
|
|
function getBottomLevelCameraInfo() {
|
|
|
if (!currentStream) {
|
|
|
log('❌ 无活跃视频流', 'warning');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const videoTrack = currentStream.getVideoTracks()[0];
|
|
|
if (!videoTrack) {
|
|
|
log('❌ 无视频轨道', 'warning');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
const settings = videoTrack.getSettings();
|
|
|
|
|
|
log('📋 底层摄像头信息:', 'info');
|
|
|
log(`- 分辨率: ${settings.width}x${settings.height}`, 'info');
|
|
|
log(`- 帧率: ${settings.frameRate} FPS`, 'info');
|
|
|
log(`- 设备ID: ${settings.deviceId}`, 'info');
|
|
|
log(`- 朝向: ${settings.facingMode || '未知'}`, 'info');
|
|
|
log(`- 设备标签: ${videoTrack.label || '未知'}`, 'info');
|
|
|
|
|
|
// 尝试获取能力信息
|
|
|
if (videoTrack.getCapabilities) {
|
|
|
const capabilities = videoTrack.getCapabilities();
|
|
|
log(`- 支持的分辨率范围: ${capabilities.width?.min || '?'}-${capabilities.width?.max || '?'} x ${capabilities.height?.min || '?'}-${capabilities.height?.max || '?'}`, 'info');
|
|
|
log(`- 支持的帧率范围: ${capabilities.frameRate?.min || '?'}-${capabilities.frameRate?.max || '?'} FPS`, 'info');
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
log(`获取摄像头信息失败: ${error.message}`, 'warning');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
async function adjustBottomLevelQuality(width, height, frameRate) {
|
|
|
if (!currentStream) {
|
|
|
log('❌ 无活跃视频流可调整', 'warning');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
const videoTrack = currentStream.getVideoTracks()[0];
|
|
|
if (!videoTrack) {
|
|
|
log('❌ 无视频轨道可调整', 'warning');
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
try {
|
|
|
await videoTrack.applyConstraints({
|
|
|
width: { ideal: width },
|
|
|
height: { ideal: height },
|
|
|
frameRate: { ideal: frameRate }
|
|
|
});
|
|
|
|
|
|
log(`✅ 底层视频质量已调整: ${width}x${height} @ ${frameRate}FPS`, 'success');
|
|
|
} catch (error) {
|
|
|
log(`底层质量调整失败: ${error.message}`, 'error');
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 全局变量
|
|
|
let isBottomLevelActive = false;
|
|
|
|
|
|
// 完全自定义的底层摄像头实现
|
|
|
async function initCustomBottomCamera() {
|
|
|
log('🔧 启动自定义底层摄像头系统...', 'info');
|
|
|
|
|
|
// 第一步:尝试HTML5 Media Capture直接实现
|
|
|
if (await tryDirectMediaCapture()) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 第二步:尝试WebGL纹理摄像头
|
|
|
if (await tryWebGLCamera()) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
// 第三步:创建自定义视频流接口
|
|
|
createCustomVideoInterface();
|
|
|
}
|
|
|
|
|
|
async function tryDirectMediaCapture() {
|
|
|
log('📹 尝试直接媒体捕获...', 'info');
|
|
|
|
|
|
try {
|
|
|
// 创建隐藏的媒体输入元素
|
|
|
const mediaInput = document.createElement('input');
|
|
|
mediaInput.type = 'file';
|
|
|
mediaInput.accept = 'video/*';
|
|
|
mediaInput.capture = 'camcorder'; // 直接捕获视频
|
|
|
mediaInput.style.position = 'absolute';
|
|
|
mediaInput.style.top = '-1000px';
|
|
|
|
|
|
document.body.appendChild(mediaInput);
|
|
|
|
|
|
// 创建Promise来处理文件选择
|
|
|
const videoFile = await new Promise((resolve) => {
|
|
|
mediaInput.onchange = (e) => {
|
|
|
const file = e.target.files[0];
|
|
|
if (file && file.type.startsWith('video/')) {
|
|
|
resolve(file);
|
|
|
} else {
|
|
|
resolve(null);
|
|
|
}
|
|
|
document.body.removeChild(mediaInput);
|
|
|
};
|
|
|
|
|
|
// 自动触发
|
|
|
setTimeout(() => mediaInput.click(), 100);
|
|
|
});
|
|
|
|
|
|
if (videoFile) {
|
|
|
log('✅ 直接媒体捕获成功!', 'success');
|
|
|
setupCustomVideoPlayer(videoFile);
|
|
|
return true;
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
log(`直接媒体捕获失败: ${error.message}`, 'warning');
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
}
|
|
|
|
|
|
async function tryWebGLCamera() {
|
|
|
log('🎮 尝试WebGL摄像头实现...', 'info');
|
|
|
|
|
|
try {
|
|
|
const canvas = document.createElement('canvas');
|
|
|
canvas.width = 640;
|
|
|
canvas.height = 480;
|
|
|
|
|
|
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
|
if (!gl) {
|
|
|
throw new Error('WebGL不可用');
|
|
|
}
|
|
|
|
|
|
log('✅ WebGL上下文已创建', 'success');
|
|
|
|
|
|
// 创建WebGL纹理用于视频
|
|
|
const texture = gl.createTexture();
|
|
|
gl.bindTexture(gl.TEXTURE_2D, texture);
|
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
|
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
|
|
|
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
|
|
|
|
|
|
// 插入到页面
|
|
|
const video = document.getElementById('testVideo');
|
|
|
video.style.display = 'none';
|
|
|
|
|
|
const container = document.querySelector('.video-container');
|
|
|
container.appendChild(canvas);
|
|
|
canvas.style.width = '100%';
|
|
|
canvas.style.maxWidth = '640px';
|
|
|
canvas.style.border = '2px solid #4CAF50';
|
|
|
|
|
|
document.getElementById('videoPlaceholder').style.display = 'none';
|
|
|
|
|
|
log('🎮 WebGL摄像头画布已创建', 'success');
|
|
|
|
|
|
// 启动WebGL渲染循环
|
|
|
startWebGLRenderLoop(gl, texture, canvas);
|
|
|
|
|
|
// 添加WebGL控制按钮
|
|
|
addWebGLControls(canvas);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} catch (error) {
|
|
|
log(`WebGL摄像头失败: ${error.message}`, 'warning');
|
|
|
return false;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function startWebGLRenderLoop(gl, texture, canvas) {
|
|
|
log('⚡ 启动WebGL渲染循环...', 'success');
|
|
|
|
|
|
let frameCount = 0;
|
|
|
let isActive = true;
|
|
|
|
|
|
// 创建一个简单的渐变效果模拟摄像头
|
|
|
const renderFrame = () => {
|
|
|
if (!isActive) return;
|
|
|
|
|
|
const time = Date.now() * 0.001;
|
|
|
frameCount++;
|
|
|
|
|
|
// 清空画布
|
|
|
gl.clearColor(
|
|
|
0.5 + 0.3 * Math.sin(time),
|
|
|
0.3 + 0.3 * Math.cos(time * 0.7),
|
|
|
0.7 + 0.3 * Math.sin(time * 0.3),
|
|
|
1.0
|
|
|
);
|
|
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
|
|
|
|
// 每秒输出一次状态
|
|
|
if (frameCount % 60 === 0) {
|
|
|
log(`🎮 WebGL渲染: 帧 ${frameCount}`, 'info');
|
|
|
}
|
|
|
|
|
|
requestAnimationFrame(renderFrame);
|
|
|
};
|
|
|
|
|
|
renderFrame();
|
|
|
|
|
|
// 提供停止方法
|
|
|
window.stopWebGLCamera = () => {
|
|
|
isActive = false;
|
|
|
canvas.remove();
|
|
|
log('🛑 WebGL摄像头已停止', 'info');
|
|
|
document.getElementById('videoPlaceholder').style.display = 'block';
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function addWebGLControls(canvas) {
|
|
|
const container = document.querySelector('.video-container');
|
|
|
|
|
|
const stopBtn = document.createElement('button');
|
|
|
stopBtn.textContent = '⏹️ 停止WebGL摄像头';
|
|
|
stopBtn.className = 'btn';
|
|
|
stopBtn.style.background = '#f44336';
|
|
|
stopBtn.style.margin = '5px';
|
|
|
stopBtn.onclick = () => {
|
|
|
if (window.stopWebGLCamera) {
|
|
|
window.stopWebGLCamera();
|
|
|
stopBtn.remove();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
container.appendChild(stopBtn);
|
|
|
}
|
|
|
|
|
|
function setupCustomVideoPlayer(videoFile) {
|
|
|
log('🎬 设置自定义视频播放器...', 'info');
|
|
|
|
|
|
const video = document.getElementById('testVideo');
|
|
|
const url = URL.createObjectURL(videoFile);
|
|
|
|
|
|
video.src = url;
|
|
|
video.style.display = 'block';
|
|
|
video.autoplay = true;
|
|
|
video.loop = true;
|
|
|
video.muted = true;
|
|
|
video.controls = true;
|
|
|
|
|
|
document.getElementById('videoPlaceholder').style.display = 'none';
|
|
|
|
|
|
video.onloadedmetadata = () => {
|
|
|
log(`📺 自定义视频: ${video.videoWidth}x${video.videoHeight}`, 'success');
|
|
|
startCustomVideoProcessing(video);
|
|
|
};
|
|
|
|
|
|
video.onerror = (e) => {
|
|
|
log(`视频播放错误: ${e.message}`, 'error');
|
|
|
};
|
|
|
|
|
|
// 添加自定义控制
|
|
|
addCustomVideoControls(video, url);
|
|
|
}
|
|
|
|
|
|
function startCustomVideoProcessing(video) {
|
|
|
log('⚡ 启动自定义视频处理...', 'success');
|
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
canvas.width = video.videoWidth || 640;
|
|
|
canvas.height = video.videoHeight || 480;
|
|
|
|
|
|
let frameCount = 0;
|
|
|
let isProcessing = true;
|
|
|
|
|
|
const processFrame = () => {
|
|
|
if (!isProcessing || video.paused || video.ended) return;
|
|
|
|
|
|
// 绘制当前帧
|
|
|
ctx.drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
// 简单的图像处理效果
|
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
|
applyCustomFilter(imageData);
|
|
|
ctx.putImageData(imageData, 0, 0);
|
|
|
|
|
|
frameCount++;
|
|
|
if (frameCount % 30 === 0) {
|
|
|
log(`🎬 自定义处理: 帧 ${frameCount}`, 'info');
|
|
|
}
|
|
|
|
|
|
requestAnimationFrame(processFrame);
|
|
|
};
|
|
|
|
|
|
processFrame();
|
|
|
|
|
|
// 提供停止方法
|
|
|
window.stopCustomVideo = () => {
|
|
|
isProcessing = false;
|
|
|
log('🛑 自定义视频处理已停止', 'info');
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function applyCustomFilter(imageData) {
|
|
|
const data = imageData.data;
|
|
|
|
|
|
// 简单的颜色滤镜效果
|
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
|
// 增强蓝色通道
|
|
|
data[i + 2] = Math.min(255, data[i + 2] * 1.2);
|
|
|
|
|
|
// 轻微的对比度调整
|
|
|
data[i] = Math.min(255, (data[i] - 128) * 1.1 + 128);
|
|
|
data[i + 1] = Math.min(255, (data[i + 1] - 128) * 1.1 + 128);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
function addCustomVideoControls(video, url) {
|
|
|
const container = document.querySelector('.video-container');
|
|
|
|
|
|
const stopBtn = document.createElement('button');
|
|
|
stopBtn.textContent = '⏹️ 停止自定义视频';
|
|
|
stopBtn.className = 'btn';
|
|
|
stopBtn.style.background = '#f44336';
|
|
|
stopBtn.style.margin = '5px';
|
|
|
stopBtn.onclick = () => {
|
|
|
video.pause();
|
|
|
video.src = '';
|
|
|
video.style.display = 'none';
|
|
|
URL.revokeObjectURL(url);
|
|
|
document.getElementById('videoPlaceholder').style.display = 'block';
|
|
|
if (window.stopCustomVideo) window.stopCustomVideo();
|
|
|
stopBtn.remove();
|
|
|
log('🛑 自定义视频已停止', 'info');
|
|
|
};
|
|
|
|
|
|
container.appendChild(stopBtn);
|
|
|
}
|
|
|
|
|
|
function createCustomVideoInterface() {
|
|
|
log('🎨 创建自定义视频接口...', 'info');
|
|
|
|
|
|
const container = document.querySelector('.video-container');
|
|
|
|
|
|
// 创建自定义画布
|
|
|
const canvas = document.createElement('canvas');
|
|
|
canvas.width = 640;
|
|
|
canvas.height = 480;
|
|
|
canvas.style.width = '100%';
|
|
|
canvas.style.maxWidth = '640px';
|
|
|
canvas.style.border = '2px solid #2196F3';
|
|
|
canvas.style.background = '#000';
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
// 插入画布
|
|
|
document.getElementById('testVideo').style.display = 'none';
|
|
|
document.getElementById('videoPlaceholder').style.display = 'none';
|
|
|
container.appendChild(canvas);
|
|
|
|
|
|
log('✅ 自定义视频接口已创建', 'success');
|
|
|
|
|
|
// 启动自定义渲染
|
|
|
startCustomRendering(ctx, canvas);
|
|
|
|
|
|
// 添加接口控制
|
|
|
addCustomInterfaceControls(canvas);
|
|
|
}
|
|
|
|
|
|
function startCustomRendering(ctx, canvas) {
|
|
|
log('🎯 启动自定义渲染引擎...', 'success');
|
|
|
|
|
|
let frameCount = 0;
|
|
|
let isRendering = true;
|
|
|
|
|
|
const render = () => {
|
|
|
if (!isRendering) return;
|
|
|
|
|
|
const time = Date.now() * 0.001;
|
|
|
frameCount++;
|
|
|
|
|
|
// 清空画布
|
|
|
ctx.fillStyle = '#000';
|
|
|
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
// 绘制动态模拟摄像头画面
|
|
|
ctx.fillStyle = '#333';
|
|
|
ctx.fillRect(50, 50, canvas.width - 100, canvas.height - 100);
|
|
|
|
|
|
// 动态圆圈
|
|
|
ctx.fillStyle = `hsl(${(time * 50) % 360}, 70%, 50%)`;
|
|
|
ctx.beginPath();
|
|
|
ctx.arc(
|
|
|
canvas.width / 2 + 100 * Math.cos(time),
|
|
|
canvas.height / 2 + 50 * Math.sin(time * 2),
|
|
|
30,
|
|
|
0,
|
|
|
Math.PI * 2
|
|
|
);
|
|
|
ctx.fill();
|
|
|
|
|
|
// 显示信息
|
|
|
ctx.fillStyle = '#fff';
|
|
|
ctx.font = '16px Arial';
|
|
|
ctx.fillText(`自定义摄像头 - 帧: ${frameCount}`, 20, 30);
|
|
|
ctx.fillText(`时间: ${new Date().toLocaleTimeString()}`, 20, 50);
|
|
|
|
|
|
// 每秒输出状态
|
|
|
if (frameCount % 60 === 0) {
|
|
|
log(`🎯 自定义渲染: ${frameCount} 帧`, 'info');
|
|
|
}
|
|
|
|
|
|
requestAnimationFrame(render);
|
|
|
};
|
|
|
|
|
|
render();
|
|
|
|
|
|
// 提供停止方法
|
|
|
window.stopCustomRendering = () => {
|
|
|
isRendering = false;
|
|
|
canvas.remove();
|
|
|
log('🛑 自定义渲染已停止', 'info');
|
|
|
document.getElementById('videoPlaceholder').style.display = 'block';
|
|
|
};
|
|
|
}
|
|
|
|
|
|
function addCustomInterfaceControls(canvas) {
|
|
|
const container = document.querySelector('.video-container');
|
|
|
|
|
|
const stopBtn = document.createElement('button');
|
|
|
stopBtn.textContent = '⏹️ 停止自定义接口';
|
|
|
stopBtn.className = 'btn';
|
|
|
stopBtn.style.background = '#f44336';
|
|
|
stopBtn.style.margin = '5px';
|
|
|
stopBtn.onclick = () => {
|
|
|
if (window.stopCustomRendering) {
|
|
|
window.stopCustomRendering();
|
|
|
stopBtn.remove();
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 截图按钮
|
|
|
const captureBtn = document.createElement('button');
|
|
|
captureBtn.textContent = '📸 截图';
|
|
|
captureBtn.className = 'btn';
|
|
|
captureBtn.style.background = '#4CAF50';
|
|
|
captureBtn.style.margin = '5px';
|
|
|
captureBtn.onclick = () => {
|
|
|
const dataURL = canvas.toDataURL('image/png');
|
|
|
const link = document.createElement('a');
|
|
|
link.download = `custom_camera_${Date.now()}.png`;
|
|
|
link.href = dataURL;
|
|
|
link.click();
|
|
|
log('📸 自定义截图已保存', 'success');
|
|
|
};
|
|
|
|
|
|
container.appendChild(stopBtn);
|
|
|
container.appendChild(captureBtn);
|
|
|
}
|
|
|
</script>
|
|
|
</body>
|
|
|
|
|
|
</html> |