使用self. 导致的内存泄露

本文深入探讨了Objective-C编程语言中对象引用的生命周期管理,特别是通过`@property`机制创建的属性如何影响内存分配与释放。通过分析代码示例,解释了在设置与获取属性值时如何避免内存泄露,以及正确使用`retain`、`release`与`autorelease`关键字的重要性。同时,提供了一个正确的实例代码,展示了如何简洁地管理和释放`self`指向的对象,确保程序资源的有效利用。

PS:在新的框架中似乎不用考虑这些了,系统应该会自动计数和释放。

请看第一段代码:
在MyObject.m中
self.aObj = [[NSObject alloc] init];
...
...
- (void)dealloc{
        [self.aObj release];
        [super dealloc];
}
你能否看出这里的内存泄露?
接下来看第二段:
aObj = [[NSObject alloc] init];
...
...
- (void)dealloc{
        [aObj release];
        [super dealloc];
}
你能否看出这里是否也有内存泄露呢?
如果你都能够很确定的知道泄露的原因,那么下面的文字,你就不需要阅读了,找个养眼的图片,养养眼吧。相反,如果你不确定,那么,下面的内容就对你很有帮助。
首先给没看出来的同学普及@property生成的set和get方法的具体内容:
- (NSObject *)aObj{
        return aObj;
}
- (void)setAObj:(NSObject *)newobj{
       if(newobj != aObj){
               [aObj release];
               aObj = [newobj retain];
       }
}
现 在,将第一段代码,带入到这个set方法中去,你就能发现 self.aObj = [[NSObject alloc] init];是一个永远不会被释放的对象,aObj这个对象所对应的指针在alloc的时候被retain了一次retainCount=1,在set方 法中又被retain了一次,所以retainCount=2,在dealloc方法中,只有一次release,所以,retainCount=1,这 个指针空间就不会被释放,而第二段代码中,并没有使用set方法,而是将 [[NSObject alloc] init];的指针付值给aObj这个变量。只有一次alloc。故在程序执行dealloc方法的时候,这个指针空间就可以被释放。
 
下面给大家一段正确使用和释放self.的例子
NSObject *newobj = [[NSObject alloc] init];
self.aObj = newobj;
[newobj release];
...
...
- (void)dealloc{
        self.aObj = nil;//这样写显的很简洁
        [super dealloc];
}
 
对于@property([copy|assign])这两种情况,大家可以参考这个链接的文章。
http://www.cocoachina.com/macdev/cocoa/2010/1015/2194.html

from PySide6 import QtWidgets, QtCore, QtGui from PyQt5.QtGui import QImage, QPixmap import cv2, os, time from threading import Thread import numpy as np from ultralytics import YOLO class MWindow(QtWidgets.QMainWindow): def __init__(self): super().__init__() # 设置 UI 界面 self.setupUI() # 连接按钮 self.camBtn.clicked.connect(self.startCamera) self.videoBtn.clicked.connect(self.startVideoFile) self.imagesBtn.clicked.connect(self.startImagesFile) self.stopBtn.clicked.connect(self.stop) self.camComboBox.currentIndexChanged.connect(self.switchCamera) self.selectWeightBtn.clicked.connect(self.selectWeightFile) # 定时器 self.timer_camera = QtCore.QTimer() self.timer_camera.timeout.connect(self.show_camera) self.timer_videoFile = QtCore.QTimer() self.timer_videoFile.timeout.connect(self.show_videoFile) self.timer_imagesFile = QtCore.QTimer() self.timer_imagesFile.timeout.connect(self.show_imagesFile) # 处理帧的队列 self.frameToAnalyze = [] Thread(target=self.frameAnalyzeThreadFunc, daemon=True).start() # 其他参数 self.cap = None self.cameras = [] self.current_camera_idx = -1 self.stopFlag = False self.vframeIdx = 0 self.model = None # 在此处初始化为空,等待选择权重文件 def load_model(self, weightPath): """加载自定义训练的 YOLO 权重文件""" if not weightPath: QtWidgets.QMessageBox.warning(self, "错误", "未选择权重文件,程序将退出") exit() try: # 使用 ultralytics.YOLO 加载权重文件 self.model = YOLO(weightPath) self.textLog.append(f"✅ 成功加载权重: {weightPath}") except Exception as e: QtWidgets.QMessageBox.critical(self, "模型加载失败", f"错误信息: {str(e)}") exit() def setupUI(self): """设置界面""" self.resize(1060, 600) self.setWindowTitle('YOLO-Qt 目标检测') mainLayout = QtWidgets.QVBox
03-13
import threading import torch from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import QThread, pyqtSignal, QObject, Qt from PyQt5.QtWidgets import QWidget, QApplication, QTableWidget, QTableWidgetItem, QLabel, QMessageBox import sys from ultralytics import YOLO class YoloTrainingThread(QObject): update_loss = pyqtSignal(float, int) log = pyqtSignal(str) finished = pyqtSignal() error = pyqtSignal(str) def __init__(self, config): super().__init__() self.config = config self.stop_signal = False self._is_running = True self.model = YOLO("./yolov8s-cls.pt") def run(self): try: self.model.add_callback('on_train_batch_end', self.on_train_batch_end) results = self.model.train(data='D:/DeepModel/ultralytics-cls/data-defects1', epochs=int(self.config['epochs']), imgsz=int(self.config['imgsz']), batch=int(self.config['batch']), optimizer="SGD", ) except Exception as e: self.error.emit(str(e)) finally: self.model.close() torch.cuda.empty_cache() def on_train_batch_end(self, trainer): """ 每批次训练结束时自动触发 """ current_loss = trainer.loss # 直接访问训练器记录的loss值 current_epoch = trainer.epoch self.update_loss.emit(current_loss, current_epoch) def train_stop(self): self._is_running = False class ui_Qwidget(QWidget): def __init__(self): super().__init__() self.setUI() self.thread = None self.config = {'data': '', 'epochs': '', 'imgsz': '', 'batch': '', } self.worker = YoloTrainingThread(self.config) self.thread = QThread() self.worker.moveToThread(self.thread) self.thread.started.connect(self.worker.run) self.worker.update_loss.connect(self.update_loss_display) def setUI(self): # 表格 self.tableWidget = QTableWidget(self) self.tableWidget.setGeometry(100, 80, 400, 300) self.tableWidget.setObjectName("tableWidget") self.tableWidget.setColumnCount(2) self.tableWidget.setRowCount(5) item = QtWidgets.QTableWidgetItem() font = QtGui.QFont() font.setPointSize(9) item.setFont(font) item = QtWidgets.QTableWidgetItem() item.setTextAlignment(QtCore.Qt.AlignCenter) self.tableWidget.setHorizontalHeaderItem(0, item) item = QtWidgets.QTableWidgetItem() item.setTextAlignment(QtCore.Qt.AlignCenter) self.tableWidget.setHorizontalHeaderItem(1, item) _translate = QtCore.QCoreApplication.translate item = self.tableWidget.horizontalHeaderItem(0) item.setText(_translate("widget", "Name")) item = self.tableWidget.horizontalHeaderItem(1) item.setText(_translate("widget", "Param")) item = QTableWidgetItem('data') self.tableWidget.setItem(0, 0, item) item = QTableWidgetItem('epochs') self.tableWidget.setItem(1, 0, item) item = QTableWidgetItem('imgsz') self.tableWidget.setItem(2, 0, item) item = QTableWidgetItem('batch') self.tableWidget.setItem(3, 0, item) item = QTableWidgetItem('train_ratio') self.tableWidget.setItem(4, 0, item) self.tableWidget.itemChanged.connect(self.TabelConnect) self.tableWidget.horizontalHeader().resizeSection(0, 100) self.tableWidget.horizontalHeader().resizeSection(1, 270) # 按钮 self.trainBtn1 = QtWidgets.QPushButton('选择文件夹', self) self.trainBtn1.setGeometry(100, 20, 100, 30) self.trainBtn2 = QtWidgets.QPushButton('新建文件夹', self) self.trainBtn2.setGeometry(230, 20, 100, 30) self.trainBtn3 = QtWidgets.QPushButton('训练', self) self.trainBtn3.setGeometry(1000, 80, 100, 30) self.trainBtn3.clicked.connect(self.trainStart) self.trainBtn4 = QtWidgets.QPushButton('推理', self) self.trainBtn4.setGeometry(1000, 140, 100, 30) self.trainBtn5 = QtWidgets.QPushButton('结束', self) self.trainBtn5.setGeometry(1000, 200, 100, 30) self.trainBtn5.clicked.connect(self.closeWindow) self.lossLabelName = QLabel('loss:', self) self.lossLabelName.setGeometry(600, 80, 100, 30) self.lossLabel = QLabel(self) self.lossLabel.setGeometry(650, 80, 100, 30) self.epochLabelName = QLabel('epoch:', self) self.epochLabelName.setGeometry(600, 120, 100, 30) self.epochLabel = QLabel(self) self.epochLabel.setGeometry(650, 120, 100, 30) self.TrainingStatus = QLabel(self) self.TrainingStatus.setGeometry(1000, 20, 100, 30) def TabelConnect(self, item): current_row = item.row() current_col = item.column() data = item.text() data1 = self.tableWidget.item(current_row, current_col - 1).text() if data1 == 'epochs': self.config['epochs'] = data elif data1 == 'imgsz': self.config['imgsz'] = data elif data1 == 'batch': self.config['batch'] = data def trainStart(self): self.trainBtn3.setEnabled(False) self.thread.start() def update_loss_display(self, loss, epoch): """更新Loss显示槽函数""" self.lossLabel.setText(f"{loss:.3f}") self.epochLabel.setText(f"{epoch}") def closeWindow(self): if self.thread and self.thread.isRunning(): self.worker.blockSignals(True) self.worker.update_loss.disconnect() self.worker.train_stop() self.thread.quit() self.thread.wait(3000) QtCore.QTimer.singleShot(1000, self.force_cleanup) def force_cleanup(self): # 释放资源 del self.worker del self.thread self.worker = None self.thread = None if __name__ == '__main__': a = QApplication(sys.argv) w = ui_Qwidget() w.setWindowTitle("训练软件") w.resize(1200, 800) w.show() a.exec() 结束训练界面会崩溃是为什么
05-14
import sys import cv2 import threading import wave import pyaudio import os import numpy as np from PyQt5.Qt import * from moviepy.editor import VideoFileClip class AudioPlayer: def __init__(self, video_path): self.video_path = video_path self.temp_audio_path = "temp_extract_audio.wav" self.p = pyaudio.PyAudio() self.stream = None self.wave_file = None self.audio_thread = None self.is_playing = False self.stop_flag = threading.Event() self.lock = threading.Lock() # 线程锁 self.extract_audio() def extract_audio(self): clip = VideoFileClip(self.video_path) audio = clip.audio if audio: audio.write_audiofile(self.temp_audio_path, fps=44100, nbytes=2, codec="pcm_s16le", verbose=False, logger=None) clip.close() def _play_audio_worker(self, start_time=0.0): try: with self.lock: self.wave_file = wave.open(self.temp_audio_path, 'rb') sample_rate = self.wave_file.getframerate() num_channels = self.wave_file.getnchannels() sampwidth = self.wave_file.getsampwidth() self.stream = self.p.open(format=self.p.get_format_from_width(sampwidth), channels=num_channels, rate=sample_rate, output=True) frame_offset = int(start_time * sample_rate) self.wave_file.setpos(min(frame_offset, self.wave_file.getnframes() - 1)) CHUNK = 1024 self.is_playing = True self.stop_flag.clear() while not self.stop_flag.is_set(): data = self.wave_file.readframes(CHUNK) if not data: break self.stream.write(data) except Exception as e: print("音频播放错误:", e) finally: self.stop() def play(self, start_time=0.0): if self.is_playing: self.stop() self.audio_thread = threading.Thread(target=self._play_audio_worker, args=(start_time,), daemon=True) self.audio_thread.start() def stop(self): with self.lock: self.stop_flag.set() self.is_playing = False try: if self.stream: self.stream.stop_stream() self.stream.close() except: pass self.stream = None try: if self.wave_file: self.wave_file.close() except: pass self.wave_file = None def close(self): self.stop() if self.p: self.p.terminate() class PrStyleVideoPlayer(QMainWindow): def __init__(self, video_path): super().__init__() self.video_path = video_path self.video_clip = None self.current_frame_idx = 0 self.total_frames = 0 self.fps = 0 self.is_playing = False self.timer = QTimer() self.audio_player = None self.timer.timeout.connect(self.play_next_frame) self.init_ui() self.load_video() def init_ui(self): self.setWindowTitle("Premiere风格视频播放器(PyAudio声音控制)") self.setGeometry(100, 100, 800, 600) central_widget = QWidget() self.setCentralWidget(central_widget) main_layout = QVBoxLayout(central_widget) # 视频显示 self.video_label = QLabel() self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setStyleSheet("background-color: black;") main_layout.addWidget(self.video_label) # 控制按钮 controls = QHBoxLayout() self.btn_prev = QPushButton("← 上一帧") self.btn_prev.clicked.connect(self.prev_frame) controls.addWidget(self.btn_prev) self.btn_play = QPushButton("▶ 播放") self.btn_play.clicked.connect(self.toggle_play_pause) controls.addWidget(self.btn_play) self.btn_next = QPushButton("下一帧 →") self.btn_next.clicked.connect(self.next_frame) controls.addWidget(self.btn_next) main_layout.addLayout(controls) # 进度显示 bottom = QHBoxLayout() bottom.addWidget(QLabel("进度")) self.slider = QSlider(Qt.Horizontal) self.slider.sliderPressed.connect(self.on_slider_pressed) self.slider.sliderReleased.connect(self.on_slider_released) self.slider.sliderMoved.connect(self.on_slider_moved) bottom.addWidget(self.slider) self.frame_label = QLabel("帧:0/0 | 时间:00:00.00") bottom.addWidget(self.frame_label) main_layout.addLayout(bottom) def load_video(self): try: self.video_clip = VideoFileClip(self.video_path) self.fps = self.video_clip.fps self.total_frames = int(self.video_clip.duration * self.fps) self.slider.setMaximum(self.total_frames - 1) self.audio_player = AudioPlayer(self.video_path) # ✅ 初始化音频播放器 self.update_frame_display() except Exception as e: QMessageBox.critical(self, "错误", f"加载视频失败: {e}") self.close() def frame_to_time(self, frame_idx): sec = frame_idx / self.fps return f"{int(sec // 60):02d}:{sec % 60:05.2f}" def update_frame_display(self): try: frame = self.video_clip.get_frame(self.current_frame_idx / self.fps) # 创建独立的内存副本 frame_copy = frame.copy() h, w, _ = frame_copy.shape bytes_per_line = 3 * w q_img = QImage(frame_copy.data, w, h, bytes_per_line, QImage.Format_RGB888) q_img = q_img.rgbSwapped() # BGR to RGB scaled = q_img.scaled(self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.video_label.setPixmap(QPixmap.fromImage(scaled)) if not self.slider.isSliderDown(): self.slider.setValue(self.current_frame_idx) self.frame_label.setText( f"帧:{self.current_frame_idx}/{self.total_frames} | 时间:{self.frame_to_time(self.current_frame_idx)}") except Exception as e: print("帧显示错误:", e) def play_next_frame(self): self.current_frame_idx += 1 if self.current_frame_idx >= self.total_frames: self.stop_playback() return self.update_frame_display() def start_playback(self): """开始播放视频 + 音频""" if self.current_frame_idx >= self.total_frames - 1: self.current_frame_idx = 0 self.is_playing = True self.btn_play.setText("⏸ 暂停") self.timer.start(int(1000 / self.fps)) start_time = self.current_frame_idx / self.fps self.audio_player.play(start_time) # ✅ 从对应时间播放音频 def stop_playback(self): """停止播放视频 + 音频""" self.is_playing = False self.btn_play.setText("▶ 播放") self.timer.stop() self.audio_player.stop() def toggle_play_pause(self): if self.is_playing: self.stop_playback() else: self.start_playback() def prev_frame(self): self.stop_playback() self.current_frame_idx = max(0, self.current_frame_idx - 1) self.update_frame_display() def next_frame(self): self.stop_playback() self.current_frame_idx = min(self.total_frames - 1, self.current_frame_idx + 1) self.update_frame_display() def on_slider_moved(self, value): self.current_frame_idx = value self.update_frame_display() def on_slider_pressed(self): if self.is_playing: self.stop_playback() def on_slider_released(self): self.current_frame_idx = self.slider.value() self.update_frame_display() def closeEvent(self, event): self.stop_playback() # 确保音频线程完全停止 if self.audio_player: self.audio_player.close() if self.audio_player.audio_thread and self.audio_player.audio_thread.is_alive(): self.audio_player.audio_thread.join(timeout=1.0) # 关闭视频剪辑 if self.video_clip: self.video_clip.close() self.video_clip = None # 删除临时音频文件 if hasattr(self.audio_player, 'temp_audio_path') and os.path.exists(self.audio_player.temp_audio_path): try: os.remove(self.audio_player.temp_audio_path) except: pass event.accept() if __name__ == "__main__": app = QApplication(sys.argv) VIDEO_PATH = r"C:/Users/Wxa/Desktop/1.mp4" # 替换为实际路径 player = PrStyleVideoPlayer(VIDEO_PATH) player.show() sys.exit(app.exec_()) 这个改成哪个更好
12-20
你的代码已经是一个功能完整的 **“Premiere 风格”视频播放器**,结合了 `PyQt5`、`OpenCV` 的图像显示逻辑(虽然你用了 MoviePy 获取帧)、`moviepy` 提取音频和关键帧,以及 `pyaudio` 实现独立音频播放线程。 但目前存在几个 **性能问题、同步问题和资源管理隐患**。我们可以从以下几个方面进行优化: --- ### ✅ 你问的是:“这个改成哪个更好?” 我的回答是:**不是“换哪个库”,而是如何重构现有代码,使其更稳定、高效、低延迟、音画同步好。** --- ## 🔧 改进目标 | 目标 | 当前问题 | 如何改进 | |------|----------|---------| | ⚠️ 音画不同步 | 视频用 QTimer 控制,音频另起线程播放,无同步机制 | 使用统一时间基准,或改用 `vlc` / `cv2 + sounddevice` 等一体化方案 | | 💥 内存泄漏风险 | `get_frame()` 反复调用未释放,且复制数据频繁 | 缓存裁剪、减少重复读取 | | 🐢 性能差(尤其大视频) | 每次都从原视频解码一帧 → 慢 | 改为使用 OpenCV 逐帧读取(支持 seek 不够灵活,但更快) | | 🗑️ 临时文件依赖 | 必须先提取 `.wav` 文件到磁盘 | 改成在内存中传递音频流(避免 I/O) | | 🛑 停止不及时 | 线程 stop_flag 不一定能立刻中断 block read | | 加超时+非阻塞模式 | --- ## ✅ 推荐重构方向:**用 OpenCV + sounddevice 替代 moviepy + pyaudio** > 因为你主要需求是:**精确控制每一帧 + 同步播放声音 + 快速跳转/拖动进度条** 而 `moviepy` 底层基于 `ffmpeg`,每次 `get_frame(t)` 都可能触发全视频解码,非常慢! --- ## ✅ 最佳替代方案:使用 `cv2.VideoCapture` + `sounddevice` 流式播放 我们来重写核心部分,大幅提升性能与响应速度。 --- ### ✅ 重构后的核心类结构(推荐) ```python import sys import cv2 import threading import numpy as np import sounddevice as sd from PyQt5.Qt import * import os class FastVideoPlayer(QMainWindow): def __init__(self, video_path): super().__init__() self.video_path = video_path self.cap = None self.audio_data = None self.sample_rate = None self.current_frame_idx = 0 self.total_frames = 0 self.fps = 0 self.is_playing = False self.timer = QTimer() self.stream = None # audio stream self.audio_position = 0 self.init_ui() self.load_video() def init_ui(self): self.setWindowTitle("高性能 Premiere 风格播放器(OpenCV + sounddevice)") self.setGeometry(100, 100, 800, 600) central_widget = QWidget() self.setCentralWidget(central_widget) layout = QVBoxLayout(central_widget) self.video_label = QLabel("加载中...") self.video_label.setAlignment(Qt.AlignCenter) self.video_label.setStyleSheet("background:black;") layout.addWidget(self.video_label) # 控制按钮 ctrl_layout = QHBoxLayout() self.btn_prev = QPushButton("← 上一帧") self.btn_prev.clicked.connect(self.prev_frame) ctrl_layout.addWidget(self.btn_prev) self.btn_play = QPushButton("▶ 播放") self.btn_play.clicked.connect(self.toggle_play_pause) ctrl_layout.addWidget(self.btn_play) self.btn_next = QPushButton("下一帧 →") self.btn_next.clicked.connect(self.next_frame) ctrl_layout.addWidget(self.btn_next) layout.addLayout(ctrl_layout) # 进度条 bottom = QHBoxLayout() bottom.addWidget(QLabel("进度")) self.slider = QSlider(Qt.Horizontal) self.slider.sliderPressed.connect(self.on_slider_pressed) self.slider.sliderReleased.connect(self.on_slider_released) self.slider.sliderMoved.connect(self.on_slider_moved) bottom.addWidget(self.slider) self.frame_label = QLabel("帧:0/0 | 时间:00:00.00") bottom.addWidget(self.frame_label) layout.addLayout(bottom) self.timer.timeout.connect(selfplay_next_frame) self.timer.setTimerType(Qt.PreciseTimer) # 更精准定时 def load_video(self): try: # === 视频加载 === self.cap = cv2.VideoCapture(self.video_path) if not self.cap.isOpened(): raise Exception("无法打开视频文件") self.fps = int(self.cap.get(cv2.CAP_PROP_FPS)) self.total_frames = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT)) self.slider.setMaximum(self.total_frames - 1) # === 提取音频到内存(不再写临时文件)=== from moviepy.editor import VideoFileClip clip = VideoFileClip(self.video_path) if clip.audio: audio_clip = clip.audio.set_fps(44100).to_soundarray(nbytes=2, quantize=True) self.audio_data = (audio_clip * 32767).astype(np.int16) # 转 PCM16 self.sample_rate = 44100 else: self.audio_data = np.zeros((int(clip.duration * 44100), 2), dtype=np.int16) self.sample_rate = 44100 clip.close() self.update_frame_display() except Exception as e: QMessageBox.critical(self, "错误", f"加载失败: {e}") print(e) def frame_to_time(self, frame_idx): sec = frame_idx / self.fps return f"{int(sec // 60):02d}:{sec % 60:05.2f}" def play_audio_callback(self, outdata, frames, time, status): if status: print(status) start = self.audio_position end = start + frames if end > len(self.audio_data): outdata[:len(self.audio_data) - start] = self.audio_data[start:] outdata[len(self.audio_data) - start:] = 0 raise sd.CallbackStop else: outdata[:] = self.audio_data[start:end] self.audio_position += frames def update_frame_display(self): self.cap.set(cv2.CAP_PROP_POS_FRAMES, self.current_frame_idx) ret, frame = self.cap.read() if not ret: self.current_frame_idx = max(0, self.total_frames - 1) return rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, c = rgb.shape q_img = QImage(rgb.data, w, h, w * c, QImage.Format_RGB888) scaled = q_img.scaled(self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation) self.video_label.setPixmap(QPixmap.fromImage(scaled)) if not self.slider.isSliderDown(): self.slider.setValue(self.current_frame_idx) self.frame_label.setText( f"帧:{self.current_frame_idx}/{self.total_frames} | 时间:{self.frame_to_time(self.current_frame_idx)}" ) def play_next_frame(self): self.current_frame_idx += 1 if self.current_frame_idx >= self.total_frames: self.stop_playback() return self.update_frame_display() def start_playback(self): if self.current_frame_idx >= self.total_frames: self.current_frame_idx = 0 self.is_playing = True self.btn_play.setText("⏸ 暂停") self.timer.start(int(1000 / self.fps)) # 启动音频流(从当前帧对应的时间开始) audio_start_sample = int((self.current_frame_idx / self.fps) * self.sample_rate) self.audio_position = audio_start_sample try: self.stream = sd.OutputStream( samplerate=self.sample_rate, channels=2, dtype='int16', callback=self.play_audio_callback, blocksize=1024 ) self.stream.start() except Exception as e: print("音频启动失败:", e) def stop_playback(self): self.is_playing = False self.btn_play.setText("▶ 播放") self.timer.stop() if self.stream: self.stream.stop() self.stream.close() self.stream = None def toggle_play_pause(self): if self.is_playing: self.stop_playback() else: self.start_playback() def prev_frame(self): self.stop_playback() self.current_frame_idx = max(0, self.current_frame_idx - 1) self.update_frame_display() def next_frame(self): self.stop_playback() self.current_frame_idx = min(self.total_frames - 1, self.current_frame_idx + 1) self.update_frame_display() def on_slider_moved(self, value): self.current_frame_idx = value self.update_frame_display() def on_slider_pressed(self): if self.is_playing: self.stop_playback() def on_slider_released(self): self.current_frame_idx = self.slider.value() self.update_frame_display() def closeEvent(self, event): self.stop_playback() if self.cap: self.cap.release() event.accept() if __name__ == "__main__": app = QApplication(sys.argv) VIDEO_PATH = r"C:/Users/Wxa/Desktop/1.mp4" player = FastVideoPlayer(VIDEO_PATH) player.show() sys.exit(app.exec_()) ``` --- ## ✅ 改进亮点说明 | 改进项 | 解决的问题 | |--------|-----------| | `cv2.VideoCapture` 替代 `VideoFileClip.get_frame()` | ⚡ 极大提升帧获取速度(支持快速 seek) | | `sounddevice.OutputStream` 实时回调播放 | 🎵 零磁盘 IO,无需生成 `.wav` 临时文件 | | 音频提前加载进内存 (`self.audio_data`) | 🔊 减少运行时解码开销 | | 使用 `blocksize=1024` 和精确回调 | 🕐 更好的音画同步潜力 | | `QTimer.PreciseTimer` | ⏱️ 定时更准确,适合视频帧率控制 | --- ## ❗ 注意事项 - `sounddevice` 需要安装:`pip install sounddevice` - 如果没有立体声,可将 `channels=2` 改为 `1` - 若视频无音频,`moviepy` 提取会报错,需加判断 - 大视频(>1GB)加载音频进内存可能会占用几百 MB → 可改为分段加载或降采样处理 --- ## ✅ 进阶建议(未来可扩展) 1. **双线程解码**:视频一个线程预加载帧,音频另一个线程播放 2. **GPU 加速解码**:使用 `cv2.cuda` 或 `decord` 库实现硬解 3. **缓存最近 N 帧**:避免反复 seek 导致卡顿 4. **支持更多格式**:通过 `ffmpeg-python` 封装通用输入 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值