惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
I
InfoQ
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 【当耐特】
小众软件
小众软件
B
Blog RSS Feed
大猫的无限游戏
大猫的无限游戏
博客园 - 三生石上(FineUI控件)
Engineering at Meta
Engineering at Meta
人人都是产品经理
人人都是产品经理
Microsoft Security Blog
Microsoft Security Blog
Last Week in AI
Last Week in AI
H
Help Net Security
爱范儿
爱范儿
云风的 BLOG
云风的 BLOG
博客园 - 司徒正美
Y
Y Combinator Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Microsoft Azure Blog
Microsoft Azure Blog
L
LangChain Blog
WordPress大学
WordPress大学
GbyAI
GbyAI
Google DeepMind News
Google DeepMind News
腾讯CDC

博客园 - 方东信

【far 语言】语言文档目录 【far 语言】注解系统 【far 语言】数组 【far 语言】并发编程(协程) 【far 语言】控制流 【far 语言】异常处理 【far 语言】farcc 构建工具 【far 语言】函数 【far 语言】组合与继承 【far 语言】接口与泛型 【far 语言】List 列表 【far 语言】Map 映射 【far 语言】原生内存与 FFI 【far 语言】字符串 【far 语言】语法总览 【far 语言】类型系统 Java 远程调用类,控制类的办法,老了,记不住了。靠 java opc ua 基于s7 音视频:JavaCV 采集桌面画面并显示的方法 UI弹窗遮罩屏蔽触发事件的处理 far的数据类型 Windows下Tesseract-OCR的安装与使用 LoRA配置 大模型三阶段训练方法(LLaMa Factory) windows系统下anaconda的安装和使用 trl ppo 【手把手从零实现】Qwen2.5-VL:环境配置/功能实验(图片识别理解、目标检测、文字OCR、文档解析、视频理解)/模型微调(MS-Swift 和 LLaMA-Factory) seata 1.8.0 报错大全 Springboot 报错 java.lang.IllegalStateException: block()/blockFirst()/blockLast() are blocking vLLM部署多模态大模型Qwen2.5-VL-3B-Instruct
pand3d实现服务端渲染并推流
方东信 · 2025-11-05 · via 博客园 - 方东信

以下代码实现了,按F1录屏+推送到kafka F2停止录屏+停止推送

import asyncio
import base64
import json
import threading
import time

import cv2
import numpy as np
from direct.actor.Actor import Actor
from direct.showbase.ShowBase import ShowBase
from fastapi import websockets
from kafka import KafkaProducer
from panda3d.core import CollisionNode, CollisionHandlerEvent, CollisionBox, CollisionSphere, CardMaker, \
    DirectionalLight, AmbientLight, WindowProperties


def add_lighting(self):
    """添加基础光照"""
    # 环境光
    alight = AmbientLight("ambient")
    alight.setColor((0.5, 0.5, 0.5, 1))
    alnp = self.render.attachNewNode(alight)
    self.render.setLight(alnp)

    # 方向光
    dlight = DirectionalLight("directional")
    dlight.setColor((0.8, 0.8, 0.8, 1))
    dlnp = self.render.attachNewNode(dlight)
    dlnp.setHpr(45, -45, 0)
    self.render.setLight(dlnp)

def create_cube(self):
    """创建简单正方体"""
    # 使用 CardMaker 创建立方体的各个面
    cm = CardMaker("cube_face")
    cm.setFrame(-1, 1, -1, 1)

    # 创建立方体节点
    self.cube = self.render.attachNewNode("cube")
    # 为正方体设置双面渲染
    self.cube.setTwoSided(True)
    # 创建6个面
    faces = []
    # 前面
    face = self.cube.attachNewNode(cm.generate())
    face.setPos(0, 1, 0)

    # 后面
    face = self.cube.attachNewNode(cm.generate())
    face.setPos(0, -1, 0)
    face.setHpr(180, 0, 0)

    # 左面
    face = self.cube.attachNewNode(cm.generate())
    face.setPos(-1, 0, 0)
    face.setHpr(90, 0, 0)

    # 右面
    face = self.cube.attachNewNode(cm.generate())
    face.setPos(1, 0, 0)
    face.setHpr(-90, 0, 0)

    # 上面
    face = self.cube.attachNewNode(cm.generate())
    face.setPos(0, 0, 1)
    face.setHpr(0, -90, 0)

    # 下面
    face = self.cube.attachNewNode(cm.generate())
    face.setPos(0, 0, -1)
    face.setHpr(0, 90, 0)

    # 设置颜色
    self.cube.setColor(0.2, 0.6, 1.0, 1.0)

# 加载模型时忽略动画数据
def load_glb_without_animation(self, file_path):
    try:
        # 尝试加载模型但不处理动画
        self.model = self.loader.loadModel(file_path)
        if self.model:
            # 移除骨骼相关节点
            self.remove_skeleton_nodes(self.model)
            self.model.reparentTo(self.render)
            return self.model
    except Exception as e:
        print(f"加载模型时出错: {e}")
        return None

def remove_skeleton_nodes(self, node_path):
    """移除骨骼相关节点"""
    # 查找并移除骨骼节点
    skeleton_nodes = node_path.findAllMatches("**/+SkeletonNode")
    for node in skeleton_nodes:
        node.removeNode()


class PandaRenderer(ShowBase):
    def __init__(self):
        ShowBase.__init__(self)
        # 或者使用更完整的方式
        from panda3d.core import WindowProperties
        wp = WindowProperties()
        wp.setTitle("实时推流")
        self.win.requestProperties(wp)


        # Kafka 配置
        self.kafka_producer = None
        self.kafka_topic = "panda3d_frames"
        self.setup_kafka()


        # 加载环境模型
        # self.environ = self.loader.loadModel("models/environment")
        # self.environ.reparentTo(self.render)
        # self.environ.setScale(0.25, 0.25, 0.25)
        # self.environ.setPos(-8, 42, 0)

        # 加载 GLB 模型
        # self.model = self.loader.loadModel("walking.glb")
        # if self.model:
        #     self.model.reparentTo(self.render)
        #     self.model.setPos(0, -15, 0)
        #     self.model.setScale(10.0)
        # else:
        #     print("模型加载失败")

        create_cube(self)
        add_lighting(self)


        self.camera.setPos(0, -15, 0)
        self.camera.lookAt(0, 0, 0)

        # 加载场景角色(一只熊猫)
        self.pandaActor = Actor(models="models/panda-model",
                                anims={"walk": "models/panda-walk4"})
        # 设置熊猫大小
        self.pandaActor.setScale(0.05, 0.05, 0.05)
        # 将熊猫加入渲染列表
        self.pandaActor.reparentTo(self.render)
        # 加入熊猫的循环动画
        self.pandaActor.loop("walk")










        #
        # # 创建一个碰撞检测节点,使用包围球
        # self.cNode = self.environ.attachNewNode(CollisionNode('player'))
        # # 创建包围球碰撞体(中心在原点,半径为1)
        # collision_sphere = CollisionSphere(0, 0, 0, 1)
        # self.cNode.node().addSolid(collision_sphere)
        #
        # # 创建碰撞检测处理器
        # self.cHandler = CollisionHandlerEvent()
        # self.cHandler.addInPattern('player-into-environment')
        #
        # # 配置碰撞节点
        # self.cNode.show()
        # self.cNode.node().setIntoCollideMask(1)
        # self.cNode.node().setFromCollideMask(128)
        # self.accept('player-into-environment', self.intoEnvironment)

        # 视频录制相关属性
        self.is_recording = False
        self.video_writer = None
        self.recorded_frames = []

        # 添加录制控制键
        self.accept('f1', self.start_recording)
        self.accept('f2', self.stop_recording)
        # 添加持续帧捕获任务
        self.frame_capture_task = None


        # 添加鼠标中键缩放功能
        self.accept('wheel_up', self.zoom_in)
        self.accept('wheel_down', self.zoom_out)

        # 初始化相机距离
        self.camera_distance = 8.0

        self.accept('arrow_left', self.rotateViewLeft)
        self.accept('arrow_right', self.rotateViewRight)
        self.accept("w", self.moveForward)


    def moveForward(self):
        print("Moving forward!")
    def intoEnvironment(self, entry):
        # 当玩家进入环境时执行的操作
        print("You have entered the environment!")
    def rotateViewLeft(self):
        self.camera.setHpr(self.camera.getHpr() + (0, -10, 0))

    def rotateViewRight(self):
        self.camera.setHpr(self.camera.getHpr() + (0, 10, 0))

    def zoom_in(self):
        """鼠标滚轮向上滚动时拉近相机"""
        self.camera_distance = max(5.0, self.camera_distance - 1.0)
        self.update_camera_position()

    def zoom_out(self):
        """鼠标滚轮向下滚动时推远相机"""
        self.camera_distance = min(50.0, self.camera_distance + 1.0)
        self.update_camera_position()

    def update_camera_position(self):
        """更新相机位置"""
        # 基于当前相机朝向和距离更新位置
        # 这里假设相机始终看向原点 (0, 0, 0)
        # from panda3d.core import Vec3
        # direction = self.camera.getPos() - Vec3(0, 0, 0)
        # direction.normalize()
        # new_pos = direction * self.camera_distance
        # self.camera.setPos(new_pos)
        if self.is_recording:
            frame = self.capture_frame()
            if frame is not None:
                self.recorded_frames.append(frame)

    def start_recording(self):
        """开始录制"""
        self.is_recording = True
        # 启动帧捕获任务
        self.frame_capture_task = self.taskMgr.add(self.capture_frame_task, "frame_capture")
        print("开始录制视频...")

    def capture_frame_task(self, task):
        """持续捕获帧的任务"""
        if self.is_recording:
            # 确保渲染完成后再捕获
            self.graphicsEngine.renderFrame()
            frame = self.capture_frame()
            if frame is not None:
                self.recorded_frames.append(frame)
                # 推送
                # 推送帧到 Kafka
                if self.kafka_producer:
                    self.broadcast_frame_kfk()

        return task.cont  # 继续执行任务

    def capture_frame(self):
        """捕获当前帧"""
        if self.win:
            # 获取屏幕截图

            screenshot = self.win.getScreenshot()
            if screenshot:
                # 转换为numpy数组
                img_data = screenshot.getRamImage().getData()
                img = np.frombuffer(img_data, dtype=np.uint8)
                img = img.reshape((screenshot.getYSize(), screenshot.getXSize(), 4))
                # 转换RGBA到BGR(OpenCV格式)
                # img = cv2.cvtColor(img, cv2.COLOR_RGBA2BGR)

                # 正确的颜色转换(从 RGBA 到 BGRA 再到 BGR)
                # 分离 alpha 通道
                b, g, r, a = cv2.split(img)
                # 重新组合为 BGR
                img = cv2.merge([b, g, r])


                # 垂直翻转图像
                img = cv2.flip(img, 0)
                return img
        return None
    def stop_recording(self):
        """停止录制并保存视频"""
        self.is_recording = False
        # 停止帧捕获任务
        if self.frame_capture_task:
            self.taskMgr.remove(self.frame_capture_task)
            self.frame_capture_task = None
        self.save_video()
        print("视频录制完成")

    def save_video(self, filename="recording.mp4"):
        """保存录制的视频"""
        if not self.recorded_frames:
            print("没有录制的帧")
            return

        # 获取第一帧的尺寸
        height, width = self.recorded_frames[0].shape[:2]

        # 创建视频写入器
        # fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        fourcc = cv2.VideoWriter_fourcc(*'XVID')
        self.video_writer = cv2.VideoWriter(
            filename,
            fourcc, 60.0, (width, height),
            True
        )

        # 写入所有帧
        for frame in self.recorded_frames:
            self.video_writer.write(frame)

        # 释放资源
        self.video_writer.release()
        self.recorded_frames = []
        print(f"视频已保存为 {filename}")



    def setup_kafka(self):
        """初始化 Kafka 生产者"""
        try:
            self.kafka_producer = KafkaProducer(
                bootstrap_servers=['127.0.0.1:9092'],
                value_serializer=lambda x: json.dumps(x).encode('utf-8')
            )
            print("Kafka 生产者初始化成功")
        except Exception as e:
            print(f"Kafka 初始化失败: {e}")

    def broadcast_frame_kfk(self):
        """通过 Kafka 广播当前帧"""
        if not self.kafka_producer:
            return

        try:
            # 捕获当前帧
            frame = self.capture_frame()

            # 编码为 JPEG
            _, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80])

            # 转换为 base64
            jpg_as_text = base64.b64encode(buffer).decode('utf-8')

            # 创建消息
            message = {
                "type": "frame",
                "data": jpg_as_text,
                "timestamp": time.time()
            }

            # 发送到 Kafka
            self.kafka_producer.send(self.kafka_topic, value=message)
            self.kafka_producer.flush()

        except Exception as e:
            print(f"Kafka 推送错误: {e}")

if __name__ == "__main__":

    renderer = PandaRenderer()

    renderer.run()

开始用python写websocket一直有问题,所有用java读取kafka 推给前端
前端代码

<!DOCTYPE html>
<html>
<head>
    <title>Kafka WebSocket 视频流客户端</title>
    <style>
        body {
            margin: 0;
            padding: 20px;
            font-family: Arial, sans-serif;
            background-color: #f0f0f0;
        }
        .container {
            max-width: 1280px;
            margin: 0 auto;
            text-align: center;
        }
        #videoCanvas {
            border: 2px solid #333;
            background-color: #000;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
        }
        .controls {
            margin: 20px 0;
        }
        button {
            padding: 10px 20px;
            font-size: 16px;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            margin: 0 10px;
        }
        button:hover {
            background-color: #45a049;
        }
        button:disabled {
            background-color: #cccccc;
            cursor: not-allowed;
        }
        .status {
            margin: 10px 0;
            padding: 10px;
            border-radius: 4px;
            font-weight: bold;
        }
        .connected {
            background-color: #d4edda;
            color: #155724;
            border: 1px solid #c3e6cb;
        }
        .disconnected {
            background-color: #f8d7da;
            color: #721c24;
            border: 1px solid #f5c6cb;
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>Kafka WebSocket 实时视频流</h1>
        
        <div class="controls">
            <button id="connectBtn">连接视频流</button>
            <button id="disconnectBtn" disabled>断开连接</button>
        </div>
        
        <div id="status" class="status disconnected">
            未连接 - 点击"连接视频流"开始接收视频
        </div>
        
        <div>
            <canvas id="videoCanvas" width="800" height="630"></canvas>
        </div>
        
        <div style="margin-top: 20px; color: #666;">
            <p>当前状态: <span id="connectionStatus">未连接</span></p>
            <p>接收帧数: <span id="frameCount">0</span></p>
        </div>
    </div>

    <script>
        class VideoStreamClient {
            constructor() {
                this.ws = null;
                this.canvas = document.getElementById('videoCanvas');
                this.ctx = this.canvas.getContext('2d');
                this.frameCount = 0;
                
                this.connectBtn = document.getElementById('connectBtn');
                this.disconnectBtn = document.getElementById('disconnectBtn');
                this.statusDiv = document.getElementById('status');
                this.connectionStatus = document.getElementById('connectionStatus');
                this.frameCountSpan = document.getElementById('frameCount');
                
                this.setupEventListeners();
            }
            
            setupEventListeners() {
                this.connectBtn.addEventListener('click', () => this.connect());
                this.disconnectBtn.addEventListener('click', () => this.disconnect());
            }
            
            connect() {
                try {
                 
                    // 连接到 WebSocket 服务
                    this.ws = new WebSocket('ws://192.168.31.190:8080/ws');
                // 在前端代码中添加更多调试信息
                    this.ws.onopen = () => {
                        console.log('WebSocket 连接已建立');
                        this.updateStatus('已连接', true);
                        this.connectBtn.disabled = true;
                        this.disconnectBtn.disabled = false;
                        this.connectionStatus.textContent = '已连接';
                        
                        // 发送测试消息
                        this.ws.send(JSON.stringify({
                            type: "ping",
                            message: "hello server"
                        }));
                    };

                    this.ws.onmessage = (event) => {
                        console.log('收到服务器消息:', event.data);
                        const data = JSON.parse(event.data);
                        if (data.type === 'frame') {
                            this.displayFrame(data.data);
                            this.frameCount++;
                            this.frameCountSpan.textContent = this.frameCount;
                        } else if (data.type === 'status') {
                            console.log('服务器状态:', data.message);
                        }
                    };
                    
                    this.ws.onerror = (error) => {
                        console.error('WebSocket 错误:', error);
                        this.updateStatus('连接错误', false);
                    };
                    
                } catch (error) {
                    console.error('连接失败:', error);
                    this.updateStatus('连接失败: ' + error.message, false);
                }
            }
            
            disconnect() {
                if (this.ws) {
                    this.ws.close();
                    this.ws = null;
                }
            }
            
            displayFrame(base64Data) {
                const img = new Image();
                img.onload = () => {
                    // 在 canvas 上绘制图像
                    this.ctx.drawImage(img, 0, 0, this.canvas.width, this.canvas.height);
                };
                img.src = 'data:image/jpeg;base64,' + base64Data;
            }
            
            updateStatus(message, isConnected) {
                this.statusDiv.textContent = message;
                this.statusDiv.className = 'status ' + (isConnected ? 'connected' : 'disconnected');
            }
        }
        
        // 页面加载完成后初始化客户端
        document.addEventListener('DOMContentLoaded', () => {
            const client = new VideoStreamClient();
            window.videoClient = client; // 便于调试
        });
    </script>
</body>
</html>

Rust编程语言群 1036955113
java新手自学群 626070845
java/springboot/hadoop/JVM 群 4915800
Hadoop/mongodb(搭建/开发/运维)Q群481975850
GOLang Q1群:6848027
GOLang Q2群:450509103
GOLang Q3群:436173132
GOLang Q4群:141984758
GOLang Q5群:215535604
C/C++/QT群 1414577
单片机嵌入式/电子电路入门群群 306312845
MUD/LIB/交流群 391486684
Electron/koa/Nodejs/express 214737701
大前端群vue/js/ts 165150391
操作系统研发群:15375777
汇编/辅助/破解新手群:755783453
大数据 elasticsearch 群 481975850
本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接,否则保留追究法律责任的权利。