You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

三星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或其他原生能力实现屏幕捕获,但缺乏入门指导。

目标

  • 实时捕获电视屏幕边缘的色彩信息,不受运行应用或电视状态影响;
  • 方案可持续高效运行,无需频繁用户干预。

咨询问题

  1. 三星Tizen电视全屏屏幕捕获的推荐方法是什么?需注意哪些特定API或权限?
  2. 使用OpenGL或Vulkan实现屏幕捕获的可行性如何?有无入门示例或文档?
  3. 如何解决屏幕内容访问的权限问题?需配置哪些特定设置或权限?

解答

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获取显示设备,创建表面后捕获帧数据。

入门实现思路:

  1. 初始化EGL/Vulkan环境并绑定到主显示;
  2. 创建帧缓冲对象,将屏幕内容渲染到纹理;
  3. 定期读取纹理像素数据,压缩后发送至Hyperion;
  4. 处理帧同步逻辑,保证低延迟捕获。

Tizen官方Native图形API文档包含基础渲染示例,可基于此修改为捕获逻辑。

3. 权限问题解决方案

权限问题核心在于Web应用沙箱限制和权限级别:

  • 若坚持使用Web应用:仅能捕获自身应用内内容,无法突破沙箱限制,建议放弃全屏捕获需求,或转为Native应用;
  • 转为Native应用:
    1. 在tizen-manifest.xml中添加正确的捕获权限;
    2. 通过三星开发者后台提交应用审核,申请合作伙伴权限(需说明全屏捕获的使用场景);
    3. 测试时使用三星提供的开发者测试证书,避免权限验证失败。

部分Tizen电视型号支持开发者模式下的调试权限,可临时开启用于测试,但正式发布仍需合作伙伴权限。

内容的提问来源于stack exchange,提问作者Gonza Castro Olaizola

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.23 14:10:55