三星Tizen电视全屏捕获与Hyperion环境光同步技术咨询
项目背景:Tizen电视屏幕捕获对接Hyperion环境光
我正在开展一个项目,需实时捕获运行Tizen OS的三星电视屏幕内容,并将色彩信息传输至连接的Hyperion服务器,以实现环境光效果。以下是项目进展、问题及咨询:
已完成工作
- 视频内容捕获:成功实现一款Web应用,可捕获应用内播放的视频内容,并以极低延迟传输至Hyperion服务器,确保从视频内容中实时、流畅地捕获色彩信息。
- 实时传输:高效处理捕获的视频帧并发送至Hyperion服务器,提供基于视频内容的无缝环境光体验。
现有实现代码(Tizen Studio可运行)
main.js
// Main document.addEventListener('DOMContentLoaded', function () { log('Document loaded and DOM fully parsed'); document.getElementById('startCapture').addEventListener('click', startCapture); document.getElementById('remoteButton').addEventListener('click', startCapture); document.getElementById('testImageButton').addEventListener('click', sendTestImage); document.getElementById('sendColorButton').addEventListener('click', sendRandomColor); document.getElementById('startVideoCaptureButton').addEventListener('click', startVideoCapture); document.getElementById('stopVideoCaptureButton').addEventListener('click', stopVideoCapture); document.addEventListener('keydown', function(event) { switch (event.key) { case 'Enter': log('Remote control button pressed'); document.activeElement.click(); break; case 'ArrowUp': focusPreviousElement(); break; case 'ArrowDown': focusNextElement(); break; } }); initializeWebSocket(); }); function focusPreviousElement() { let focusable = document.querySelectorAll('[tabindex]'); let index = Array.prototype.indexOf.call(focusable, document.activeElement); if (index > 0) { focusable[index - 1].focus(); } } function focusNextElement() { let focusable = document.querySelectorAll('[tabindex]'); let index = Array.prototype.indexOf.call(focusable, document.activeElement); if (index < focusable.length - 1) { focusable[index + 1].focus(); } } function startCapture() { log('Start Capture button clicked'); captureScreen(); } function captureScreen() { log('Capturing screen'); const captureArea = document.getElementById('captureArea'); const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); const fixedWidth = 120; const fixedHeight = 120; canvas.width = fixedWidth; canvas.height = fixedHeight; try { // Colores alternativos para asegurar que la imagen no sea negra const colors = ['red', 'green', 'blue', 'yellow', 'purple', 'orange']; const randomColor = colors[Math.floor(Math.random() * colors.length)]; context.fillStyle = randomColor; context.fillRect(0, 0, canvas.width, canvas.height); const text = captureArea.innerText || captureArea.textContent; context.font = '30px Arial'; context.fillStyle = 'white'; context.textAlign = 'center'; context.textBaseline = 'middle'; context.fillText(text, canvas.width / 2, canvas.height / 2); const imageData = canvas.toDataURL('image/jpeg', 0.03).split(',')[1]; // Lower quality to 0.03 log('Screenshot captured, Base64 length: ' + imageData.length); sendToHyperion(imageData); } catch (error) { log('Error capturing screen: ' + error); } } function sendTestImage() { log('Sending test image to Hyperion'); const testImageUrl = 'https://eligeeducar.cl/content/uploads/2019/10/Imagen-que-dice-rojo-sobre-un-fondo-azul-y-azul-sobre-un-fondo-rojo--1920x550.jpg'; fetch(testImageUrl) .then(response => response.blob()) .then(blob => compressImage(blob)) .then(imageData => sendToHyperion(imageData)) .catch(error => { log('Error fetching test image: ' + error); }); } function compressImage(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = function () { const img = new Image(); img.onload = function () { const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const maxWidth = 120; const maxHeight = 120; let width = img.width; let height = img.height; if (width > height) { if (width > maxWidth) { height *= maxWidth / width; width = maxWidth; } } else { if (height > maxHeight) { width *= maxHeight / height; height = maxHeight; } } canvas.width = width; canvas.height = height; ctx.drawImage(img, 0, 0, width, height); const imageData = canvas.toDataURL('image/jpeg', 0.03).split(',')[1]; // Lower quality to 0.03 log('Compressed image captured, Base64 length: ' + imageData.length); resolve(imageData); }; img.onerror = reject; img.src = reader.result; }; reader.onerror = reject; reader.readAsDataURL(blob); }); } let ws; let messageQueue = []; function sendToHyperion(imageData) { const jsonData = { command: "image", imagedata: imageData, name: "TizenScreenCapture", format: "auto", origin: "TizenApp", priority: 50, duration: 30000 // Duración de 30 segundos }; log('Sending screenshot to Hyperion'); if (ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify(jsonData)); } else { messageQueue.push(JSON.stringify(jsonData)); } } function initializeWebSocket() { const ipHyperion = '192.168.88.101'; const puertoHyperion = 8090; ws = new WebSocket(`ws://${ipHyperion}:${puertoHyperion}/jsonrpc`); ws.onopen = function () { log('WebSocket connection opened'); while (messageQueue.length > 0) { ws.send(messageQueue.shift()); } }; ws.onmessage = function (event) { log('Message from server: ' + event.data); }; ws.onerror = function (error) { log('WebSocket error: ' + error.message); }; ws.onclose = function () { log('WebSocket connection closed'); // Intentar reconectar después de 1 segundo setTimeout(initializeWebSocket, 1000); }; } function sendRandomColor() { const ipHyperion = '192.168.88.101'; const puertoHyperion = 8090; const colors = [ [255, 0, 0], // Red [0, 255, 0], // Green [0, 0, 255], // Blue [255, 255, 0], // Yellow [255, 0, 255], // Magenta [0, 255, 255] // Cyan ]; const randomColor = colors[Math.floor(Math.random() * colors.length)]; const jsonData = { command: "color", color: randomColor, duration: 120000, // 2 minutes priority: 20, origin: "TizenApp" }; log('Sending color to Hyperion: ' + JSON.stringify(jsonData)); fetch(`http://${ipHyperion}:${puertoHyperion}/json-rpc`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(jsonData) }) .then(response => response.json()) .then(data => log('Response from Hyperion: ' + JSON.stringify(data))) .catch(error => log('Error sending color to Hyperion: ' + error)); } // Configuración del reproductor de YouTube let player; function onYouTubeIframeAPIReady() { player = new YT.Player('player', { height: '390', width: '640', videoId: 'Gt6wKDnG0xA', events: { 'onReady': onPlayerReady, 'onStateChange': onPlayerStateChange } }); } let captureInterval; function startVideoCapture() { if (!player || !player.getIframe) { log('YouTube Player not ready'); return; } log('Starting video capture'); const iframe = player.getIframe(); const videoElement = iframe.contentWindow.document.querySelector('video'); if (!videoElement) { log('Video element not found'); return; } const canvas = document.createElement('canvas'); const context = canvas.getContext('2d'); canvas.width = 120; canvas.height = 120; captureInterval = setInterval(() => { try { context.drawImage(videoElement, 0, 0, canvas.width, canvas.height); const imageData = canvas.toDataURL('image/jpeg', 0.03).split(',')[1]; // Lower quality to 0.03 sendToHyperion(imageData); } catch (error) { log('Error capturing video frame: ' + error); clearInterval(captureInterval); } }, 100); // Capturar cada 100 ms } function stopVideoCapture() { log('Stopping video capture'); clearInterval(captureInterval); } function onPlayerReady(event) { event.target.playVideo(); } function onPlayerStateChange(event) { if (event.data == YT.PlayerState.PLAYING) { log('YouTube Player playing'); startVideoCapture(); } else { log('YouTube Player paused or ended'); stopVideoCapture(); } } function log(message) { const logs = document.getElementById('logs'); const logMessage = document.createElement('p'); logMessage.textContent = message; logs.appendChild(logMessage); console.log(message); }
index.html
<!DOCTYPE html> <html> <head> <title>SamyGrabber</title> <style> body { font-family: Arial, sans-serif; background-color: #121212; color: white; text-align: center; padding: 20px; } #captureArea { width: 100%; height: 300px; border: 1px solid #ccc; margin-bottom: 20px; display: flex; justify-content: center; align-items: center; } #logs { background-color: #222; padding: 10px; border-radius: 5px; margin-top: 20px; max-height: 200px; overflow-y: scroll; text-align: left; } .button-container { display: flex; justify-content: center; gap: 20px; } button { padding: 10px 20px; font-size: 16px; border: none; border-radius: 5px; background-color: #6200ee; color: white; cursor: pointer; } button:hover { background-color: #3700b3; } </style> </head> <body> <h1>SamyGrabber</h1> <div id="captureArea" contenteditable="true"> <span>Texto</span> </div> <div class="button-container"> <button id="startCapture" tabindex="1">Iniciar Captura</button> <button id="remoteButton" tabindex="2">Captura con Control Remoto</button> <button id="testImageButton" tabindex="3">Enviar Imagen de Prueba</button> <button id="sendColorButton" tabindex="4">Enviar Color Aleatorio</button> <button id="startVideoCaptureButton" tabindex="5">Iniciar Captura de Video</button> <button id="stopVideoCaptureButton" tabindex="6">Detener Captura de Video</button> </div> <div id="logs"></div> <div id="player"></div> <script src="https://www.youtube.com/iframe_api"></script> <script src="main.js"></script> </body> </html>
面临的挑战
- Web应用限制:当前方案仅能捕获Web应用内的内容,无法捕获电视全屏内容(包括Web应用未聚焦或未运行时)。
- 权限问题:使用
captureScreen等API时遭遇权限错误,即使已在config.xml中设置必要权限。 - 替代方案探索:正尝试使用OpenGL、Vulkan等底层图形API或其他原生能力实现屏幕捕获,但缺乏入门指导。
目标
- 实时捕获电视屏幕边缘的色彩信息,不受运行应用或电视状态影响;
- 方案可持续高效运行,无需频繁用户干预。
咨询问题
- 三星Tizen电视全屏屏幕捕获的推荐方法是什么?需注意哪些特定API或权限?
- 使用OpenGL或Vulkan实现屏幕捕获的可行性如何?有无入门示例或文档?
- 如何解决屏幕内容访问的权限问题?需配置哪些特定设置或权限?
解答
1. 全屏屏幕捕获的推荐方法与API/权限
Tizen电视的全屏捕获仅支持Native C/C++应用,Web应用受沙箱限制无法跨应用访问屏幕内容,推荐使用以下Native API:
- Screen Capture API(Tizen 4.0+):
screen_capture.h中的接口,支持捕获整个屏幕或指定显示区域; - Display API:通过
display_get_default()获取主显示设备,配合帧缓冲读取实现捕获。
权限配置(需在tizen-manifest.xml中添加):
<privilege>http://tizen.org/privilege/screen.capture</privilege> <privilege>http://tizen.org/privilege/mediacontroller.client</privilege>
注意:该权限仅对三星合作伙伴级别签名的应用开放,普通开发者需申请三星开发者计划权限,否则无法调用全屏捕获接口。
2. OpenGL/Vulkan实现屏幕捕获的可行性与入门
完全可行,底层图形API可直接读取显示帧缓冲,但需基于Native应用开发:
- OpenGL ES:通过
EGLImageKHR绑定显示帧缓冲,将屏幕内容渲染到纹理后读取像素数据; - Vulkan:使用
VkDisplayKHR获取显示设备,创建表面后捕获帧数据。
入门实现思路:
- 初始化EGL/Vulkan环境并绑定到主显示;
- 创建帧缓冲对象,将屏幕内容渲染到纹理;
- 定期读取纹理像素数据,压缩后发送至Hyperion;
- 处理帧同步逻辑,保证低延迟捕获。
Tizen官方Native图形API文档包含基础渲染示例,可基于此修改为捕获逻辑。
3. 权限问题解决方案
权限问题核心在于Web应用沙箱限制和权限级别:
- 若坚持使用Web应用:仅能捕获自身应用内内容,无法突破沙箱限制,建议放弃全屏捕获需求,或转为Native应用;
- 转为Native应用:
- 在
tizen-manifest.xml中添加正确的捕获权限; - 通过三星开发者后台提交应用审核,申请合作伙伴权限(需说明全屏捕获的使用场景);
- 测试时使用三星提供的开发者测试证书,避免权限验证失败。
- 在
部分Tizen电视型号支持开发者模式下的调试权限,可临时开启用于测试,但正式发布仍需合作伙伴权限。
内容的提问来源于stack exchange,提问作者Gonza Castro Olaizola
相关产品推荐
相关产品推荐

