我试图允许用户将场景导出为mp4(视频格式),场景的项目由QGraphicsVideoItem和多个QGraphicsTextItem组成,我需要导出场景,因为它将允许用户保存视频文本项目。我找到了一种方法来做到这一点,但问题是,一个简单的 5 秒视频需要几个小时,因为它将每个图像保存到一个字节来创建视频,每个图像都是一毫秒。如果我从毫秒更改为秒,它可以加快速度,但视频看起来不会那么流畅,是否有更有效的方法来做到这一点,而不需要这么长时间?
from PySide6.QtCore import *
from PySide6.QtGui import *
from PySide6.QtWidgets import *
from PySide6.QtSvgWidgets import *
from PySide6.QtMultimediaWidgets import QGraphicsVideoItem
from PySide6.QtMultimedia import QMediaPlayer, QAudioOutput, QMediaMetaData
import subprocess
import sys
class ExportVideo(QThread):
def __init__(self, video_item, video_player, graphics_scene, graphics_view):
super().__init__()
self.video_item = video_item
self.video_player = video_player
self.graphics_scene = graphics_scene
self.graphics_view = graphics_view
def run(self):
self.video_player.pause()
duration = self.video_player.duration()
meta = self.video_player.metaData()
# Prepare a pipe for ffmpeg to write to
ffmpeg_process = subprocess.Popen(['ffmpeg', '-y', '-f', 'image2pipe', '-r', '1000', '-i', '-', '-c:v', 'libx265', '-pix_fmt', 'yuv420p', 'output.mp4'], stdin=subprocess.PIPE)
for duration in range(0, duration):
self.video_player.setPosition(duration)
# Add logic to render the frame here
print("Exporting frame:", duration)
image = QImage(self.graphics_scene.sceneRect().size().toSize(), QImage.Format_ARGB32)
painter = QPainter(image)
self.graphics_scene.render(painter)
painter.end()
# Convert QImage to bytes
byte_array = QByteArray()
buffer = QBuffer(byte_array)
buffer.open(QIODevice.WriteOnly)
image.save(buffer, 'JPEG')
# Write image bytes to ffmpeg process
ffmpeg_process.stdin.write(byte_array.data())
# Close the pipe to signal ffmpeg that all frames have been processed
ffmpeg_process.stdin.close()
ffmpeg_process.wait()
class PyVideoPlayer(QWidget):
def __init__(self):
super().__init__()
self.text_data = []
self.mediaPlayer = QMediaPlayer()
self.audioOutput = QAudioOutput()
self.graphics_view = QGraphicsView()
self.graphic_scene = QGraphicsScene()
self.graphics_view.setScene(self.graphic_scene)
self.graphic_scene.setBackgroundBrush(Qt.black)
self.graphics_view.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.graphics_view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
self.video_item = QGraphicsVideoItem()
self.graphic_scene.addItem(self.video_item)
self.save_video = QPushButton()
layout = QVBoxLayout()
layout.addWidget(self.graphics_view, stretch=1)
layout.addWidget(self.save_video)
self.setLayout(layout)
# Slots Section
self.mediaPlayer.setVideoOutput(self.video_item)
self.mediaPlayer.positionChanged.connect(self.changeVideoPosition)
self.save_video.clicked.connect(self.saveVideo)
def setMedia(self, fileName):
self.mediaPlayer.setSource(QUrl.fromLocalFile(fileName))
self.mediaPlayer.setAudioOutput(self.audioOutput)
self.play()
self.video_item.setSize(self.mediaPlayer.videoSink().videoSize())
self.text_item = QGraphicsTextItem()
self.text_item.setPlainText("Test Dummy")
self.text_item.setDefaultTextColor(Qt.white)
font = QFont()
font.setPointSize(90)
self.text_item.setFont(font)
self.text_item.setPos(self.graphic_scene.sceneRect().x() + self.text_item.boundingRect().width(), self.graphic_scene.sceneRect().center().y() - self.text_item.boundingRect().height())
self.graphic_scene.addItem(self.text_item)
self.text_data.append("Test Dummy")
def play(self):
if self.mediaPlayer.playbackState() == QMediaPlayer.PlaybackState.PlayingState:
self.mediaPlayer.pause()
else:
self.mediaPlayer.play()
def changeVideoPosition(self, duration):
if duration > 1000 and self.text_item.isVisible():
print("Hide Text")
self.text_item.hide()
def resize_graphic_scene(self):
self.graphics_view.fitInView(self.graphic_scene.sceneRect(), Qt.KeepAspectRatio)
def showEvent(self, event):
self.resize_graphic_scene()
def resizeEvent(self, event):
self.resize_graphic_scene()
def saveVideo(self):
self.videoExport = ExportVideo(self.video_item, self.mediaPlayer, self.graphic_scene, self.graphics_view)
self.videoExport.start()
if __name__ == "__main__":
app = QApplication(sys.argv)
window = PyVideoPlayer()
window.setMedia("example.mp4")
window.setGeometry(100, 100, 400, 300) # Set the window size
window.setWindowTitle("QGraphicsView Example")
window.show()
sys.exit(app.exec())
Assertion fctx->async_lock failed at C:/ffmpeg-n6.0/libavcodec/pthread_frame.c:155.
我认为这一定与设置视频播放器位置有关。我认为您使用当前方法(UI 屏幕捕获来生成带注释的视频)将面临的问题是您可能会丢失原始视频分辨率(假设它是 1080p 但显示在 300x400 UI 帧中,那么您将最终得到 300x400 视频而不是 1080p)。
IMO 更好的方法是在 FFmpeg(或任何其他方式)中生成具有匹配帧分辨率(在我的示例中为 1080p)的具有透明背景的文本图像(PNG),并将每个此类图像作为 QGraphicItem 加载到 QGraphicScene 上(我认为可以拖动它来重新定位)。
这将为您提供背景视频和一堆文本图像及其显示时间和位置偏移。然后,FFmpeg 可以处理将这些文件合并在一起。
要生成文本PNG,您可以运行
import subprocess as sp
video_size = [1920, 1080]
text = "hello world"
color = "black"
fontsize = 30
fontfile = "Freeserif.ttf"
textfile = "temp_01.png" # place it in a temp folder & increment
sp.run(
[
"ffmpeg",
"-f", "lavfi",
"-i", f"color=c={color}@0:size={video_size[0]}x{video_size[1]},format=rgba",
"-vf", f'drawtext=fontsize={fontsize}:fontfile={fontfile}:text=\'{text}\':x=(w-text_w)/2:y=(h-text_h)/2',
"-update", "1", "-vframes", "1",
"-y", # if needed to overwrite old
textfile,
]
)
此脚本创建一个在屏幕中间带有“hello world”的 PNG。假设我们想将此文本放在视频的时间戳 1 和 2 之间,用户定义的偏移量为 [-200px,100px](朝向左下角)。
text_start = 1
text_end = 2
text_xoff = -200
text_yoff = 100
videofile = "example.mp4"
outfile = "annotated.mp4"
sp.run(
[
"ffmpeg",
"-i", videofile, # [0:v]
"-i", textfile, # [1:v]
"-filter_complex", f"[0:v][1:v]overlay=x={text_xoff}:y={text_yoff}:enable='between(t,{text_start},{text_end})'[out]",
"-map", "[out]",
'-y', # again, if need to overwrite previous output
outfile,
]
)
如果要叠加多个文本图像,则需要列出所有文本文件作为附加输入 (
-i ...
) 并级联运行叠加过滤器:
[0:v][1:v]overlay=...[out1];
[out1][2:v]overlay=...[out2];
...
[outN][N:v]overlay=...[out]
显然,您希望以编程方式生成过滤器图表达式。
color
、format
、drawtext
、overlay
。熟悉这些过滤器和生成过程中的过滤器图结构,尤其要注意字符转义。 (提示:将文本放在单引号中,并在覆盖文本中转义单引号)
注释
overlay
过滤器支持文本图像随时间移动,因此可以设置动画,但在 Qt 上显示动画会很痛苦我对 Qt 业务端不熟悉。所以,我就把这个留给你了。
欢迎在评论区提问。