Я пытаюсь разрешить пользователю экспортировать сцену в формате 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.
Я думаю, это должно быть связано с настройкой положения видеоплеера.Текст фиксирован на время?
Оба подхода совершенно неэффективны. Если пользователь ничего не делает в течение нескольких секунд (или даже минут/часов), обработка сохранит тысячи одинаковых изображений, неоправданно занимая память и процессор. Более подходящая концепция могла бы хранить пару данных с использованием (монотонной) метки времени и соответствующего отображаемого контента, обновляемого вместе с сигналом QGraphicsScene.changed. Поскольку вам не нужен рендеринг в реальном времени, вы можете завершить вывод с помощью соответствующих команд ffmpeg, используя различные «ключевые кадры» в зависимости от их временных меток.
Не могли бы вы показать мне пример улучшений, которые вы предлагаете? Текст не будет исправлен, поскольку у него будет время начала и окончания в зависимости от времени видео, а позже появится анимация.
Я не знаю, как с этим справиться на стороне Qt, но наиболее эффективный подход — сделать снимки только текстовых объектов (индивидуально) в виде изображений и наложить их на видео в FFmpeg с его графом фильтров. Таким образом, вы не будете тратить время на захват видеокадров. Я подготовлю ответ позже, если этот подход заинтересует.
Еще один вопрос. Вам важно, чтобы формат текста (шрифт, размер и т.д.) соответствовал тому, что на экране? Если нет, вы можете просто использовать фильтр drawtext.
Да, пользователь сможет изменить формат текста, и текст будет разным.
@kesh, я пытался сделать то, что вы предлагали раньше, но продолжал получать сообщение об ошибке и вернулся к захвату видео и текста. Я думаю, способ, который вы предлагаете, вероятно, будет более эффективным.
@Alex Еще одна вещь, которая имеет чрезвычайную важность: элементы пользовательского интерфейса не являются потокобезопасными. Их не только нельзя создать или «записать» (изменить их свойства) из отдельных потоков, но и доступ к ним в «режиме чтения» по-прежнему небезопасен и совершенно ненадежен. Кроме того, воспроизведение видео может быть очень требовательным, а это означает, что попытка захватить все содержимое сцены может привести к задержкам не только записи, но и воспроизведения. Python не очень хорошо справляется с требовательными задачами в реальном времени, поэтому у вас есть только два варианта: 1. если вам не нужна идеально плавная запись, то »
@Alex » просто используйте комбинацию QTimer и вышеупомянутого сигнала changed
в качестве оптимизации; это все равно приведет к потенциальным задержкам при воспроизведении приложения во время записи, если ЦП не сможет справиться с этой задачей; 2. если вам нужно более безопасное (всё ещё в зависимости от ЦП) плавное воспроизведение и запись, то придётся переключаться на многопроцессорность или совсем на другой язык. В любом случае остается то, что объяснялось выше: 1. непрерывная запись не имеет смысла; 2. вы не должны использовать потоки для рендеринга виджета.
Я думаю, что у меня может быть лучший способ экспортировать видео, поскольку пользователь не сможет изменить видео только отображаемый текст, я пытаюсь сделать так, чтобы он захватывал каждый текстовый формат и время начала/окончания. , который будет использовать эту информацию для создания текста поверх видео с помощью drawtext из ffmpeg. Вероятно, будут ограничения по настройке, но я думаю, что это будет более эффективно, чем использование функции рендеринга из сцены, и не будет так требовательно к процессору.
Можете ли вы просто сохранить текстовый графический объект в формате PNG с прозрачным фоном? Я думаю, это было бы лучшее из обоих миров
Я пробовал это, и, поскольку на заднем плане есть видео, оно экспортирует текст и видео, поскольку единственный способ рендеринга, который я знаю, - это с помощью Scene.render, и он визуализирует все, что находится в пределах ваших границ. Я запросил рендеринг, поэтому, если за текстом что-то есть, он также это визуализирует. Я пытаюсь внести изменения, предложенные musicamante, и если я не смогу, мне, возможно, придется придерживаться вашего предложения или написать текст непосредственно в видео.
@musicamante воспроизведение и экспорт видео не обязательно должны происходить одновременно, если вы подразумеваете это под «воспроизведением и записью». На данный момент у меня есть поток, меняющий положение видео, чтобы текст знал, когда больше не показывать и воспроизводить следующую часть видео. Однако, если не требуется устанавливать положение видео для захвата видеокадра и текста, не могли бы вы сообщить мне, как это сделать, спасибо.
Извините, судя по вашему первоначальному вопросу, вы хотели сделать «скриншот» этой сцены. На самом деле вы просто хотите добавить элементы в видео, а это значит, что рендеринг QGraphicsScene не только бессмысленен, но и совершенно неверен. Вам следует сохранить данные логического объекта, которые вы хотите нарисовать (геометрические элементы, содержимое, внешний вид временной шкалы и т. д.), и в конечном итоге экспортировать их, используя исходное видео, а не элемент видеографики.
Я думаю, что проблема, с которой вы столкнетесь при нынешнем подходе (снимок экрана пользовательского интерфейса для создания аннотированного видео), заключается в том, что вы, скорее всего, потеряете исходное разрешение видео (скажем, если оно 1080p, но отображается в кадре пользовательского интерфейса 300x400, тогда вы закончите с видео 300x400, а не 1080p).
Лучшим подходом IMO является создание текстового изображения (PNG) с прозрачным фоном в FFmpeg (или любым другим способом) с соответствующим разрешением кадра (например, 1080p в моем примере) и загрузка каждого из таких изображений в QGraphicScene как QGraphicItem ( который, как я полагаю, можно перетаскивать, чтобы изменить положение).
Это даст вам фоновое видео и несколько текстовых изображений с указанием времени их отображения и смещения положения. Затем 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,
]
)
Этот скрипт создает PNG с надписью «Hello World» в центре экрана. Допустим, мы хотим поместить этот текст между временными метками 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]
Очевидно, вы хотите сгенерировать выражение графа фильтра программным способом.
Вот ссылки на используемые фильтры цвет , формат , drawtext , наложение. Ознакомьтесь с этими фильтрами и конструкцией графа фильтров в целом (см. верхнюю часть связанной страницы документа), особенно обратите внимание на экранирование символов. (подсказка: поместите текст в одинарные кавычки и избегайте одинарных кавычек в наложенном тексте)
Примечания
overlay
фильтр поддерживает перемещение текстового изображения во времени, поэтому его можно анимировать, но отображение анимации на Qt было бы затруднительноЯ не знаком с Qt-концом бизнеса. Итак, я оставлю это вам.
Не стесняйтесь задавать вопросы в разделе комментариев.
Я думаю, поскольку текст создается в изображении, а затем добавляется в видео, я мог бы просто нарисовать текст непосредственно в видео? Я не уверен, как я буду поддерживать формат, например, некоторые символы в тексте QGraphicsTextItem имеют разные цвета, жирность и т. д. Однако, вероятно, есть способ сделать это.
Вы, конечно, можете сразу приступить к записи текста на видео, но у меня сложилось впечатление, что вы хотите, чтобы пользователь мог размещать/форматировать текст в пользовательском интерфейсе. Здесь я предлагаю вместо того, чтобы позволить Qt визуализировать текст, визуализировать его с помощью FFmpeg и отобразить результат в виде плавающих изображений на холсте с видео в качестве задней панели, если это имеет смысл.
Кое-что из этого поста. Тогда вы все равно сможете перемещать текст в Qt. Однако необходимо создать новое текстовое изображение, если пользователь хочет изменить текстовый формат.
Я должен был быть более ясным: пользователи должны иметь возможность регулярно менять формат и положение текста (аналогично видеоредактору), а создавать новый текст изображения каждый раз, когда им нужно что-то изменить, было бы неэффективно, я изучаю что @musicamante предложил по оптимизации с помощью многопроцессорности, однако спасибо за предложение.
Попался. Возможно, это не так дорого, как вы думаете. Я бы предложил попробовать генерацию изображения, чтобы увидеть его скорость, прежде чем отказываться от этого пути. Удачи!
Исправление, которое я нашел, не использует PyQt для основного экспорта видео, а использует только FFMPEG. Я знал, что FFMPEG не поддерживает формат Rich Text Format (RTF), поэтому начал искать другие возможные способы решения этой проблемы. Я обнаружил, что вы можете использовать формат файла под названием Advanced SubStation Alpha (.ASS), который позволяет вам контролировать все, что угодно. делать с текстом, например шрифт, размер, цвет, время начала/окончания и многое другое.
При использовании .ASS вам необходимо убедиться, что вы правильно форматируете файл .ASS, иначе FFMPEG может не записать субтитры так, как вы хотите. Вам также понадобится текст, который вы хотите отобразить в видео, в формате HTML, что можно сделать с помощью pyqt text.toHtml()
.
Когда у вас есть путь к местоположению текстовой версии HTML, вам также понадобятся размер/цвет обводки и начало/конец текста. Вы также можете отредактировать его, включив в него положение текста. Вам придется вызвать функцию с исходным видео.
from bs4 import BeautifulSoup
import subprocess, sys
from PySide6.QtCore import *
from PySide6.QtWidgets import *
class videoExport(QThread):
def __init__(self, text_and_stroke, video_location):
super().__init__()
self.text_and_stroke = text_and_stroke
self.video_location = video_location
def run(self):
dialogues = []
for html_file_path, pos_x, pos_y, stroke_size, stroke_color, start, end in self.text_and_stroke:
with open(html_file_path, 'r') as file:
html_content = file.read()
soup = BeautifulSoup(html_content, 'html.parser')
body_tag = soup.find('body')
if body_tag:
self.styles_attributes = self.retrieve_text_style(body_tag.get('style'))
text_style = self.text_styles(soup, stroke_size, stroke_color)
self.styles_attributes = ",".join(self.styles_attributes)
text_style = "".join(text_style)
new_dialogue = f"""Dialogue: {start},{end},Default,{{\pos({pos_x},{pos_y})}}{text_style}"""
dialogues.append(new_dialogue)
file.close()
create_ass_file = f"""[Script Info]
Title: Video Subtitles
ScriptType: v4.00+
Collisions: Normal
PlayDepth: 0
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BorderStyle, Encoding
Style: Default, {self.styles_attributes},&HFFB0B0,&HFFFF00,&H998877,0,0
Style: Background, {self.styles_attributes},&H00FFFFFF,&H000000FF,&H00000000,3,0
[Events]
Format: Start, End, Style, Text
"""
for dialogue in dialogues:
create_ass_file += f"{dialogue}\n"
with open("new_ass.ass", 'w') as file:
file.write(create_ass_file)
file.close()
self.add_subtitle_to_video(self.video_location, "new_ass.ass")
def add_subtitle_to_video(self, video_file, ass_file):
video_text = [
"ffmpeg",
"-y",
"-i", video_file,
"-vf", f"subtitles = {ass_file}",
"-c:a", "copy",
"output.mp4"
]
process = subprocess.Popen(video_text, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,universal_newlines=True)
# ADDS ANY TEXT STYLE TO THE TEXT
# ///////////////////////////////////////////////////////////////
def text_styles(self, soup, stroke_size, stroke_color):
text_styles = []
paragraph = soup.find_all('p')
span_elements = soup.find_all('span')
if paragraph:
for paragraph_element in paragraph:
for content in paragraph_element.contents:
if isinstance(content, str):
written_text = content.strip()
text_styles.append(written_text)
if span_elements:
for span in span_elements:
style_attr = span.get('style')
style_and_name = style_attr.split(';')
# Initialize variables to hold style modifications
italic_str = underline_str = ""
color_str = background_color_str = text_stroke = ""
font_weight_str = font_size_str = ""
font_family_str = ""
for text in style_and_name:
if ':' in text:
style = text.split(':')[0].strip()
style_name = text.split(':')[1].strip().replace("'", '')
if stroke_size > 0:
stroke_color = stroke_color.replace('#', '')
red = int(stroke_color[0:2], 16)
green = int(stroke_color[2:4], 16)
blue = int(stroke_color[4:6], 16)
text_stroke = f"\\bord{stroke_size}\\3c&H{blue:02X}{green:02X}{red:02X}&"
# Modify text based on style
if style_name == 'italic':
italic_str = "{\\i1}"
elif style_name == 'underline':
underline_str = "{\\u1}"
elif style == 'color':
hex_color = style_name.replace('#', '')
red = int(hex_color[0:2], 16)
green = int(hex_color[2:4], 16)
blue = int(hex_color[4:6], 16)
color_str = f"\\c&H{blue:02X}{green:02X}{red:02X}&"
elif style == 'background-color' and stroke_size <= 0:
hex_color = style_name.replace('#', '')
red = int(hex_color[0:2], 16)
green = int(hex_color[2:4], 16)
blue = int(hex_color[4:6], 16)
background_color_str = f"\\rBackground\\bord1\\3c&H{blue:02X}{green:02X}{red:02X}&"
elif style == 'font-weight':
font_weight_str = "{\\b1}"
elif style == 'font-size':
font_size = style_name.replace('pt', '')
font_size_str = f"\\fs{font_size}"
elif style == 'font-family':
font_family_str = f"\\fn{style_name}"
# Combine all style modifications
text_style = "{" + text_stroke + background_color_str + color_str + italic_str + underline_str + font_size_str + font_family_str + font_weight_str + "}"
text_with_styles = f"{text_style}{span.text.strip()}{{\\r}}"
# Append modified text to the list
text_styles.append(text_with_styles)
return text_styles
# Retrive the basic text style attributes
# ///////////////////////////////////////////////////////////////
def retrieve_text_style(self, style_attr):
style_and_name = style_attr.split(';')
style_attributes = []
for text in style_and_name:
if ':' in text:
style_name = text.split(':')[1].strip().replace("'", '')
if 'pt' in style_name:
style_name = style_name.replace('pt', '')
style_attributes.append(style_name)
break
style_attributes.append(style_name)
return style_attributes
class MainWindow(QMainWindow):
def __init__(self):
super().__init__()
self.setWindowTitle("Video Exporter")
self.export_button = QPushButton("Export Video")
self.export_button.clicked.connect(self.export_video)
layout = QVBoxLayout()
layout.addWidget(self.export_button)
central_widget = QWidget()
central_widget.setLayout(layout)
self.setCentralWidget(central_widget)
def export_video(self):
text_and_stroke = [("new_subtitle.html", "0", "0", 10, "#FFFFFF", "0:00:00.00", "0:00:05.00")]
self.export_thread = videoExport(text_and_stroke, "other.mp4")
self.export_thread.finished.connect(self.on_export_finished)
self.export_thread.start()
def on_export_finished(self):
QMessageBox.information(self, "Export Finished", "Video export completed!")
if __name__ == "__main__":
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
Недостатком этого способа является то, что если у вас есть текстовая анимация в вашей сцене, вам придется научиться конвертировать ее в анимацию .ASS, что должно быть возможно, также если у вас есть нетекстовые анимации в вашей QGraphicsScene и вы хотите экспортировать их как MP4, вам, вероятно, придется использовать многопроцессорную обработку для захвата сцены (могут быть и другие способы). Обводка текста применима только в том случае, если вы можете ее получить. Я не уверен, есть ли способ использовать базовый QGraphicsTextItem, но поскольку я не использую систему контуров Qt, а вместо этого рисую свою собственную, я могу ее получить.
Еще немного информации о .ASS, которая мне показалась полезной, и о том, как я создаю контурный текст: https://stackoverflow.com/a/78362730/22802649https://hhsprings.bitbucket.io/docs/programming/examples/ffmpeg/subtitle/ass.html
Я заметил, что image.save(buffer, 'PNG') - это процесс, который занимает больше времени и замедляет весь процесс. Если есть более эффективный способ получения байтовых массивов изображения, можно ускорить создание видео. , Я думаю.