python写的rtsp码流录制工具

[复制链接]
105 |10
发表于 前天 23:57 | 显示全部楼层 |阅读模式
运行环境,windows10,python3.13.2。
需要提前创建tmp和video文件夹。


运行截图




代码
[Python]  
  1. import sys
  2. import os
  3. import threading
  4. import cv2
  5. import datetime
  6. from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
  7.                             QHBoxLayout, QLabel, QLineEdit, QPushButton,
  8.                             QTextEdit, QFrame, QMessageBox, QSplitter)
  9. from PyQt5.QtGui import QImage, QPixmap, QFont
  10. from PyQt5.QtCore import Qt, QTimer, pyqtSignal, pyqtSlot
  11. class RTSPRecorder(QMainWindow):
  12.     # 定义信号用于线程间通信
  13.     log_signal = pyqtSignal(str)
  14.     preview_signal = pyqtSignal(object)
  15.     def __init__(self):
  16.         super().__init__()
  17.         self.rtsp_url = ""
  18.         self.is_recording = False
  19.         self.cap = None
  20.         self.output_file = ""
  21.         self.log_messages = []
  22.         # 先初始化UI,确保log_text控件先创建
  23.         self.init_ui()
  24.         # 连接信号和槽
  25.         self.log_signal.connect(self.add_log_slot)
  26.         self.preview_signal.connect(self.update_preview_slot)
  27.         # 然后再创建必要的目录
  28.         self.create_directories()
  29.         # 定时器用于更新预览
  30.         self.timer = QTimer(self)
  31.         self.timer.timeout.connect(self.update_preview)
  32.         # 录制线程
  33.         self.record_thread = None
  34.     def create_directories(self):
  35.         """创建video和tmp目录"""
  36.         try:
  37.             # 获取脚本所在目录的绝对路径,而不是依赖当前工作目录
  38.             script_dir = os.path.dirname(os.path.abspath(__file__))
  39.             self.video_dir = os.path.join(script_dir, "video")
  40.             self.tmp_dir = os.path.join(script_dir, "tmp")
  41.             self.log_signal.emit(f"脚本目录: {script_dir}")
  42.             self.log_signal.emit(f"视频目录将创建在: {self.video_dir}")
  43.             self.log_signal.emit(f"临时目录将创建在: {self.tmp_dir}")
  44.             # 确保父目录存在
  45.             os.makedirs(script_dir, exist_ok=True)
  46.             if not os.path.exists(self.video_dir):
  47.                 os.makedirs(self.video_dir, exist_ok=True)
  48.                 self.log_signal.emit(f"成功创建视频目录: {self.video_dir}")
  49.             else:
  50.                 self.log_signal.emit(f"视频目录已存在: {self.video_dir}")
  51.             if not os.path.exists(self.tmp_dir):
  52.                 os.makedirs(self.tmp_dir, exist_ok=True)
  53.                 self.log_signal.emit(f"成功创建临时目录: {self.tmp_dir}")
  54.             else:
  55.                 self.log_signal.emit(f"临时目录已存在: {self.tmp_dir}")
  56.             # 测试目录写入权限
  57.             test_file = os.path.join(self.video_dir, "test_permission.txt")
  58.             with open(test_file, "w") as f:
  59.                 f.write("Permission test")
  60.             os.remove(test_file)
  61.             self.log_signal.emit(f"成功测试目录写入权限")
  62.         except Exception as e:
  63.             error_msg = f"无法创建或访问必要的目录: {str(e)}"
  64.             self.log_signal.emit(error_msg)
  65.             # 尝试使用系统临时目录作为备选
  66.             try:
  67.                 self.video_dir = os.path.join(os.environ.get("TEMP", "/tmp"), "rtsp_video")
  68.                 self.tmp_dir = os.path.join(os.environ.get("TEMP", "/tmp"), "rtsp_tmp")
  69.                 os.makedirs(self.video_dir, exist_ok=True)
  70.                 os.makedirs(self.tmp_dir, exist_ok=True)
  71.                 self.log_signal.emit(f"使用系统临时目录作为备选: {self.video_dir}")
  72.                 self.log_signal.emit(f"使用系统临时目录作为备选: {self.tmp_dir}")
  73.             except Exception as e2:
  74.                 QMessageBox.critical(self, "目录创建错误",
  75.                                     f"无法创建必要的目录:\n{error_msg}\n\n" \
  76.                                     f"尝试使用系统临时目录也失败:\n{str(e2)}")
  77.                 sys.exit(1)
  78.     def init_ui(self):
  79.         """初始化用户界面"""
  80.         self.setWindowTitle("RTSP 录制工具")
  81.         self.setGeometry(100, 100, 800, 600)
  82.         # 设置中文字体
  83.         font = QFont()
  84.         font.setFamily("SimHei")
  85.         self.setFont(font)
  86.         # 主布局
  87.         main_widget = QWidget()
  88.         main_layout = QVBoxLayout(main_widget)
  89.         # RTSP地址输入区域
  90.         rtsp_layout = QHBoxLayout()
  91.         rtsp_label = QLabel("RTSP地址:")
  92.         self.rtsp_input = QLineEdit()
  93.         self.rtsp_input.setPlaceholderText("rtsp://username:password@ip:port/path")
  94.         self.rtsp_input.setFont(font)
  95.         rtsp_layout.addWidget(rtsp_label)
  96.         rtsp_layout.addWidget(self.rtsp_input)
  97.         # 按钮区域
  98.         button_layout = QHBoxLayout()
  99.         self.start_btn = QPushButton("开始录制")
  100.         self.start_btn.clicked.connect(self.start_recording)
  101.         self.start_btn.setFont(font)
  102.         self.stop_btn = QPushButton("停止录制")
  103.         self.stop_btn.clicked.connect(self.stop_recording)
  104.         self.stop_btn.setEnabled(False)
  105.         self.stop_btn.setFont(font)
  106.         button_layout.addWidget(self.start_btn)
  107.         button_layout.addWidget(self.stop_btn)
  108.         # 预览窗口
  109.         preview_frame = QFrame()
  110.         preview_frame.setFrameShape(QFrame.StyledPanel)
  111.         preview_layout = QVBoxLayout(preview_frame)
  112.         self.preview_label = QLabel("视频预览")
  113.         self.preview_label.setAlignment(Qt.AlignCenter)
  114.         self.preview_label.setMinimumHeight(300)
  115.         self.preview_label.setFont(font)
  116.         preview_layout.addWidget(self.preview_label)
  117.         # 日志区域
  118.         log_frame = QFrame()
  119.         log_frame.setFrameShape(QFrame.StyledPanel)
  120.         log_layout = QVBoxLayout(log_frame)
  121.         log_label = QLabel("运行日志")
  122.         log_label.setFont(font)
  123.         self.log_text = QTextEdit()
  124.         self.log_text.setReadOnly(True)
  125.         self.log_text.setFont(font)
  126.         log_layout.addWidget(log_label)
  127.         log_layout.addWidget(self.log_text)
  128.         # 使用分隔器布局预览和日志区域
  129.         splitter = QSplitter(Qt.Vertical)
  130.         splitter.addWidget(preview_frame)
  131.         splitter.addWidget(log_frame)
  132.         splitter.setSizes([300, 200])
  133.         # 添加所有组件到主布局
  134.         main_layout.addLayout(rtsp_layout)
  135.         main_layout.addLayout(button_layout)
  136.         main_layout.addWidget(splitter)
  137.         self.setCentralWidget(main_widget)
  138.     @pyqtSlot(str)
  139.     def add_log_slot(self, message):
  140.         """添加日志信息的槽函数,确保在主线程中执行"""
  141.         timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
  142.         log_entry = f"[{timestamp}] {message}"
  143.         self.log_messages.append(log_entry)
  144.         self.log_text.append(log_entry)
  145.         # 自动滚动到底部
  146.         self.log_text.verticalScrollBar().setValue(
  147.             self.log_text.verticalScrollBar().maximum()
  148.         )
  149.     @pyqtSlot(object)
  150.     def update_preview_slot(self, frame):
  151.         """更新视频预览的槽函数,确保在主线程中执行"""
  152.         if frame is not None:
  153.             # 转换为RGB格式
  154.             rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
  155.             # 转换为QImage
  156.             h, w, c = rgb_frame.shape
  157.             q_img = QImage(rgb_frame.data, w, h, w * c, QImage.Format_RGB888)
  158.             # 缩放图像以适应预览窗口
  159.             pixmap = QPixmap.fromImage(q_img).scaled(
  160.                 self.preview_label.width(),
  161.                 self.preview_label.height(),
  162.                 Qt.KeepAspectRatio,
  163.                 Qt.SmoothTransformation
  164.             )
  165.             self.preview_label.setPixmap(pixmap)
  166.     def add_log(self, message):
  167.         """添加日志信息(为了兼容旧代码而保留)"""
  168.         self.log_signal.emit(message)
  169.     def start_recording(self):
  170.         """开始录制RTSP流"""
  171.         self.rtsp_url = self.rtsp_input.text().strip()
  172.         if not self.rtsp_url:
  173.             QMessageBox.warning(self, "警告", "请输入RTSP地址")
  174.             return
  175.         self.is_recording = True
  176.         self.start_btn.setEnabled(False)
  177.         self.stop_btn.setEnabled(True)
  178.         # 创建录制线程
  179.         self.record_thread = threading.Thread(target=self.record_stream)
  180.         self.record_thread.daemon = True
  181.         self.record_thread.start()
  182.         # 启动预览定时器
  183.         self.timer.start(30)  # 大约33fps
  184.         self.log_signal.emit(f"开始录制RTSP流: {self.rtsp_url}")
  185.     def stop_recording(self):
  186.         """停止录制RTSP流"""
  187.         if not self.is_recording:
  188.             return
  189.         self.is_recording = False
  190.         self.start_btn.setEnabled(True)
  191.         self.stop_btn.setEnabled(False)
  192.         # 停止定时器
  193.         self.timer.stop()
  194.         # 等待录制线程结束
  195.         if self.record_thread and self.record_thread.is_alive():
  196.             self.record_thread.join(timeout=3.0)  # 设置超时,避免卡死
  197.         # 释放资源
  198.         if self.cap:
  199.             self.cap.release()
  200.             self.cap = None
  201.         # 清理tmp目录
  202.         self.clean_tmp_directory()
  203.         self.log_signal.emit(f"停止录制,文件保存至: {self.output_file}")
  204.         self.preview_label.setText("视频预览")
  205.     def record_stream(self):
  206.         """录制RTSP流的线程函数"""
  207.         try:
  208.             # 禁用OpenCV的多线程,避免FFmpeg错误
  209.             cv2.setNumThreads(0)
  210.             # 打开RTSP流
  211.             self.cap = cv2.VideoCapture(self.rtsp_url)
  212.             if not self.cap.isOpened():
  213.                 self.log_signal.emit("无法打开RTSP流,请检查地址是否正确")
  214.                 # 在录制线程中使用信号停止录制
  215.                 self.is_recording = False
  216.                 # 通过调用主线程的stop_recording来清理资源
  217.                 QApplication.instance().invokeMethod(self, "stop_recording", Qt.QueuedConnection)
  218.                 return
  219.             # 获取视频信息
  220.             fps = self.cap.get(cv2.CAP_PROP_FPS)
  221.             width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
  222.             height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
  223.             self.log_signal.emit(f"视频信息 - 分辨率: {width}x{height}, FPS: {fps:.2f}")
  224.             # 创建输出文件名
  225.             timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
  226.             self.output_file = os.path.join(self.video_dir, f"recording_{timestamp}.mp4")
  227.             # 定义编码器和创建VideoWriter对象
  228.             fourcc = cv2.VideoWriter_fourcc(*'mp4v')
  229.             out = cv2.VideoWriter(self.output_file, fourcc, fps, (width, height))
  230.             if not out.isOpened():
  231.                 self.log_signal.emit("无法创建输出视频文件,请检查权限")
  232.                 self.is_recording = False
  233.                 QApplication.instance().invokeMethod(self, "stop_recording", Qt.QueuedConnection)
  234.                 return
  235.             # 缓存文件
  236.             tmp_file = os.path.join(self.tmp_dir, f"temp_{timestamp}.mp4")
  237.             tmp_out = cv2.VideoWriter(tmp_file, fourcc, fps, (width, height))
  238.             frame_count = 0
  239.             error_count = 0
  240.             last_preview_time = datetime.datetime.now()
  241.             preview_interval = datetime.timedelta(milliseconds=100)  # 每100ms更新一次预览
  242.             while self.is_recording:
  243.                 ret, frame = self.cap.read()
  244.                 if not ret:
  245.                     error_count += 1
  246.                     self.log_signal.emit(f"无法接收帧 {error_count} 次,可能是网络问题")
  247.                     if error_count > 10:  # 连续10次错误则停止
  248.                         self.log_signal.emit("连续多次无法接收帧,停止录制")
  249.                         break
  250.                 else:
  251.                     error_count = 0  # 重置错误计数
  252.                     # 写入正式文件和缓存文件
  253.                     out.write(frame)
  254.                     tmp_out.write(frame)
  255.                     frame_count += 1
  256.                     if frame_count % 100 == 0:  # 每100帧记录一次
  257.                         self.log_signal.emit(f"已录制 {frame_count} 帧")
  258.                     # 控制预览频率,避免UI更新太频繁
  259.                     current_time = datetime.datetime.now()
  260.                     if current_time - last_preview_time > preview_interval:
  261.                         # 发送帧给预览槽函数
  262.                         self.preview_signal.emit(frame.copy())
  263.                         last_preview_time = current_time
  264.             # 释放资源
  265.             out.release()
  266.             tmp_out.release()
  267.             self.log_signal.emit(f"录制完成,共录制 {frame_count} 帧")
  268.         except Exception as e:
  269.             self.log_signal.emit(f"录制过程中发生错误: {str(e)}")
  270.         finally:
  271.             # 确保释放资源
  272.             if hasattr(self, 'cap') and self.cap:
  273.                 self.cap.release()
  274.                 self.cap = None
  275.     def update_preview(self):
  276.         """更新视频预览窗口(已修改为通过信号槽机制实现)"""
  277.         # 此方法现在基本为空,实际预览逻辑已移至record_stream方法中通过preview_signal实现
  278.         pass
  279.     def clean_tmp_directory(self):
  280.         """清理tmp目录"""
  281.         try:
  282.             for filename in os.listdir(self.tmp_dir):
  283.                 file_path = os.path.join(self.tmp_dir, filename)
  284.                 try:
  285.                     if os.path.isfile(file_path) or os.path.islink(file_path):
  286.                         os.unlink(file_path)
  287.                         self.log_signal.emit(f"删除临时文件: {filename}")
  288.                     elif os.path.isdir(file_path):
  289.                         # 如果是目录,递归删除
  290.                         import shutil
  291.                         shutil.rmtree(file_path)
  292.                         self.log_signal.emit(f"删除临时目录: {filename}")
  293.                 except Exception as e:
  294.                     self.log_signal.emit(f"清理文件 {file_path} 时出错: {str(e)}")
  295.             self.log_signal.emit("已清理临时文件目录")
  296.         except Exception as e:
  297.             self.log_signal.emit(f"清理临时目录时出错: {str(e)}")
  298.     def closeEvent(self, event):
  299.         """窗口关闭事件处理"""
  300.         if self.is_recording:
  301.             reply = QMessageBox.question(
  302.                 self, "确认", "正在录制中,确定要退出吗?",
  303.                 QMessageBox.Yes | QMessageBox.No, QMessageBox.No
  304.             )
  305.             if reply == QMessageBox.Yes:
  306.                 self.stop_recording()
  307.                 event.accept()
  308.             else:
  309.                 event.ignore()
  310.         else:
  311.             event.accept()
  312. if __name__ == "__main__":
  313.     app = QApplication(sys.argv)
  314.     # 设置全局字体,确保中文正常显示
  315.     font = QFont("SimHei")
  316.     app.setFont(font)
  317.     window = RTSPRecorder()
  318.     window.show()
  319.     sys.exit(app.exec_())
复制代码



附上代码常见报错解决办法,感谢cxqdly  的分享
运行脚本遇到三个问题以及解决办法:
1. ModuleNotFoundError: No module named 'cv2'
错误原因:OpenCV 库未安装
解决办法:pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple


2. ModuleNotFoundError: No module named 'PyQt5'
错误原因:PyQt5 未安装
解决办法:pip install PyQt5 -i https://pypi.tuna.tsinghua.edu.cn/simple


3. qt.qpa.plugin: Could not find the Qt platform plugin "windows" in ""
错误原因:未配置环境变量
解决办法:新建系统变量QT_QPA_PLATFORM_PLUGIN_PATH,路径为PyQt5/Qt/plugins/platforms 目录中qwindows.dll对应的地址


4. 如果第三步配置变量后运行还报一样的错误,关闭cmd窗口后重新打开运行即可显示rtsp运行的界面

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

发表于 前天 23:58 | 显示全部楼层
运行脚本遇到三个问题以及解决办法:
1. ModuleNotFoundError: No module named 'cv2'
错误原因:OpenCV 库未安装
解决办法:pip install opencv-python -i https://pypi.tuna.tsinghua.edu.cn/simple


2. ModuleNotFoundError: No module named 'PyQt5'
错误原因:PyQt5 未安装
解决办法:pip install PyQt5 -i https://pypi.tuna.tsinghua.edu.cn/simple


3. qt.qpa.plugin: Could not find the Qt platform plugin "windows" in ""
错误原因:未配置环境变量
解决办法:新建系统变量QT_QPA_PLATFORM_PLUGIN_PATH,路径为PyQt5/Qt/plugins/platforms 目录中qwindows.dll对应的地址


4. 如果第三步配置变量后运行还报一样的错误,关闭cmd窗口后重新打开运行即可显示rtsp运行的界面
回复

使用道具 举报

发表于 前天 23:58 | 显示全部楼层
rtsp地址一般从摄像机获取,各厂家的摄像机码流格式都不一样的,在网上搜,找到了可以放VLC里面先播放试试。
回复

使用道具 举报

发表于 前天 23:59 | 显示全部楼层
这样的话,电脑可以作为录播来使用了。楼主高手

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

发表于 前天 23:59 | 显示全部楼层
666,摄像头电脑客户端了,搞个4画面的,直接电脑监控、录像了
回复

使用道具 举报

发表于 昨天 00:00 | 显示全部楼层
不错哦,相当于录像机了
回复

使用道具 举报

发表于 昨天 00:01 | 显示全部楼层
不错啊,代码整齐,是ai辅助的吧
回复

使用道具 举报

发表于 昨天 00:01 | 显示全部楼层
感谢楼主的热心分享
回复

使用道具 举报

发表于 昨天 00:02 | 显示全部楼层
大神;可以运行了;RTSP地址怎么获取啊?
回复

使用道具 举报

发表于 昨天 00:02 | 显示全部楼层
PIP 下我也是出现这问题
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

72小时热榜
热门版块
热门帖子
1
自用剪贴板
12038陌
2025-12-07
2
京东时间同步器新增淘宝时间2.0
骑着悟空看八戒
2025-12-07
4
python写的rtsp码流录制工具
骑着悟空看八戒
2025-12-06
6
Web打印工具
12038陌
2025-12-06
快速回复 返回顶部 返回列表