Pygame结合OpenCV播放视频:解决拖动窗口导致的音画不同步问题
本文介绍如何在Pygame中利用OpenCV读取视频帧并播放,重点解决拖动窗口时因帧率波动导致的音画不同步问题。通过计算时间差并引入跳帧机制,确保音频与视频画面保持同步,适用于音游等对节奏敏感的场景。
Pygame作为SDL的Python绑定,因其易用性而广受欢迎。它简化了多媒体编程,有效避免了内存泄漏等常见陷阱,让开发者能更专注于逻辑实现。
然而,Pygame内置的Movie模块已废弃多年,若在项目中需要嵌入视频播放,可能会面临技术选型困难。曾尝试使用Ren'py推荐的libav方案,但pyav库的使用复杂度较高,耗时良久仍未完全掌握。最终决定采用OpenCV直接读取视频数据,以解决这一难题。
实现该功能主要依赖以下第三方库:
- pygame
- numpy
- opencv-python
其中,numpy通常随Anaconda环境预装,无需单独安装;而pygame和opencv-python均可通过pip直接安装获取。
在Pygame中结合OpenCV或moviepy播放视频是常见做法。核心原理在于利用pygame.surfarray接口将numpy数组转换为Surface对象,而视频读取库可将视频帧转换为numpy矩阵,两者结合即可实现图像渲染。具体代码实现见下文。
测试视频选自《龙背上的骑兵3》(ドラッグオンドラグーン)的音乐游戏片段。音频部分可直接使用Pygame的音频模块播放。需注意,视频帧率设置必须与源视频一致,本例中为25fps。建议设置略高的帧率(如25.5fps),以预留缓冲空间,避免因处理延迟导致丢帧。
常规游戏逻辑中通常不会嵌入视频播放,因此建议将视频播放逻辑独立处理,以防止帧率跌破设定值(如25fps)。尽管现代计算机性能足以应对,但独立处理仍是稳妥之举。
运行效果如图所示:

角色设计依然备受喜爱。
以下是基础实现代码:
# -*- coding:utf-8 -*-
import pygame
import sys
import cv2
import numpy as np
import os
if __name__ == '__main__':
pygame.init() # 初始化pygame
FPS = 25.5
#设置窗口位置
os.environ['SDL_VIDEO_WINDOW_POS']="%d,%d" % (50,70)
FPSClock = pygame.time.Clock()
size = width, height = 1280, 720 # 设置窗口大小
screen = pygame.display.set_mode(size) # 显示窗口
pygame.display.set_caption(u'打字游戏:反应练习')
color = (255, 255, 255) # 设置颜色
ogg=pygame.mixer.Sound("game.ogg")
# pygame.mixer.music.load("")
videoCapture = cv2.VideoCapture("test.mp4")
ogg.play()
while True:
a=pygame.time.get_ticks()
if videoCapture.isOpened():
#从opncv读一段视频进来
ret, frame = videoCapture.read()
#下面这句可以获取图像大小
# print('frame.shape:', frame.shape)
#
# rot90(m, k=1, axes=(0, 1)
# m是要旋转的数组(矩阵),
# k是旋转的次数,默认旋转1次,
# 那是顺时针还是逆时针呢?
# 正数表示逆时针,而k为负数时则是对数组进行顺时针方向的旋转。
# 视频结束时会丢出一个ValueError异常
try:
frame = np.rot90(frame,k=-1)
except:
continue
frame = pygame.surfarray.make_surface(frame)
frame=pygame.transform.flip(frame,False,True)
# frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
screen.blit(frame, (0,0))
# print()
for event in pygame.event.get(): # 遍历所有事件
if event.type == pygame.QUIT: # 如果单击关闭窗口,则退出
sys.exit()
pygame.display.flip() # 更新全部显示
time_next= FPSClock.tick(FPS)
b=pygame.time.get_ticks()
pygame.quit() # 退出pygame
上述代码实现了Pygame对视频的基本播放功能。
但这并非完美方案。
当手动拖动窗口时,程序会卡在拖动前的最后一帧,而音频继续播放。直到鼠标松开,视频画面才恢复更新,这导致严重的音画不同步现象,画面滞后于音频。
特别是在音乐游戏中,音画不同步会直接破坏节奏体验,导致游戏无法正常进行。
这一问题亟待解决。
接下来探讨解决方案。
核心在于时间控制,掌握时间同步即可解决全局问题。
提出以下两种处理方案:
方案1:基于时间差的跳帧机制。通过计算帧间时间差,若超过特定阈值(如100ms),判定为发生停滞,随即执行跳帧。考虑到人类操作手速有限,可适当增大阈值,以规避CPU波动造成的误判。 方案2:重置音频播放。根据视频播放时间重新同步音频,使其回到对应状态。该方法实现简单,但精度较低,需权衡使用。 理论上,跳帧方案更优,因为视频帧数大,误差影响小,且能保持音频的连续性。
经过对比,最终选择方案一。
具体算法如下: 定义变量: 时间参量 = 下一帧时间 - 上一帧时间 跳帧计数器 = 0(初始化于循环外) 阈值 = 200(当时间参量超过此值,判定为异常) 当检测到阈值超标时,执行跳帧算法: 计算 故障持续时间 / 正常帧间隔,得出需跳过的帧数。增加计数器至该数值,并执行跳帧操作。
以下是集成跳帧器的完整代码:
# -*- coding:utf-8 -*-
import pygame
import sys
import cv2
import numpy as np
import os
# 音频同步跳帧实验
# 新增:跳帧系统
if __name__ == '__main__':
pygame.init() # 初始化pygame
FPS = 25.5
# 设置窗口位置
os.environ['SDL_VIDEO_WINDOW_POS'] = "%d,%d" % (50, 70)
FPSClock = pygame.time.Clock()
size = width, height = 1280, 720 # 设置窗口大小
# screen = pygame.display.set_mode(size,pygame.FULLSCREEN) # 显示窗口
screen = pygame.display.set_mode(size) # 显示窗口
pygame.display.set_caption(u'打字游戏:反应练习')
color = (255, 255, 255) # 设置颜色
ogg = pygame.mixer.Sound("game.ogg")
# pygame.mixer.music.load("")
videoCapture = cv2.VideoCapture("test.mp4")
ogg.play()
# 真正时间
realtime = int(1000 / FPS)
# 阈值-要稍微留下空间
jumpyu = 100
# 跳帧计数器
jumpnum = 0
while True:
a = pygame.time.get_ticks()
if videoCapture.isOpened():
#这里是跳帧工具
if jumpnum != 0:
#这个函数光读东西,比read少一个解码过程,所以能省点算点
videoCapture.grab()
jumpnum -= 1
continue
ret, frame = videoCapture.read()
try:
frame = np.rot90(frame, k=-1)
except:
continue
frame = pygame.surfarray.make_surface(frame)
frame = pygame.transform.flip(frame, False, True)
# frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
screen.blit(frame, (0, 0))
# print()
for event in pygame.event.get(): # 遍历所有事件
if event.type == pygame.QUIT: # 如果单击关闭窗口,则退出
sys.exit()
pygame.display.flip() # 更新全部显示
time_next = FPSClock.tick(FPS)
b = pygame.time.get_ticks()
k = b - a
#此处检测阈值,大于阈值做记录准备跳帧
if k > jumpyu:
print('超过阈值,跳帧')
temp = (k - realtime)/realtime
tempstr=str(temp)
tempstr=tempstr[tempstr.find('.')+1:]
if len(tempstr)>1 or tempstr[0]!='0':
jumpnum+=(int(temp)+1)
print('跳小数帧')
else:
print('跳整数帧')
jumpnum+=int(temp)
print('跳'+str(jumpnum)+'帧')
pygame.quit() # 退出pygame
测试结果显示,即使多次拖动窗口,系统也能智能执行跳帧,保持音画同步。

至此,通过引入跳帧机制,将拖动窗口导致的音画不同步问题降至最低。虽然无法做到帧级别的绝对精确(全屏与窗口切换仍有帧损失),但对于视频播放已足够。建议尽量避免频繁拖动窗口,以获得最佳体验。
本文介绍了Pygame与OpenCV联合播放视频并解决音画同步问题的方法。更多相关技术细节,请参考本站其他文章或关注后续更新。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









