Created
February 12, 2025 08:04
-
-
Save downthecrop/9ca8ea0cda6c14d4eb5c962b410c9a94 to your computer and use it in GitHub Desktop.
PyQt6 WebM Player With Transparency
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import sys | |
import subprocess | |
import json | |
import numpy as np | |
from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QLabel, QPushButton, QVBoxLayout, QHBoxLayout, QFileDialog | |
from PyQt6.QtGui import QImage, QPixmap | |
from PyQt6.QtCore import Qt, QTimer | |
# Supports transparent WebM videos in VP8 or VP9 format. | |
# https://simpl.info/videoalpha/video/dancer1.webm | |
# https://www.remotion.dev/img/transparent-video.webm | |
def get_video_info(file_path): | |
cmd = [ | |
"ffprobe", | |
"-v", "error", | |
"-select_streams", "v:0", | |
"-print_format", "json", | |
"-show_entries", "stream=width,height", | |
file_path, | |
] | |
try: | |
output = subprocess.check_output(cmd, stderr=subprocess.STDOUT) | |
info = json.loads(output) | |
streams = info.get("streams", []) | |
if streams: | |
width = streams[0].get("width") | |
height = streams[0].get("height") | |
return width, height | |
except subprocess.CalledProcessError as e: | |
print("ffprobe error:", e.output) | |
return None, None | |
class VideoPlayerWidget(QWidget): | |
def __init__(self, parent=None): | |
super().__init__(parent) | |
self.imageLabel = QLabel("No video loaded") | |
self.imageLabel.setAlignment(Qt.AlignmentFlag.AlignCenter) | |
self.prevButton = QPushButton("Previous") | |
self.playButton = QPushButton("Play") | |
self.nextButton = QPushButton("Next") | |
self.prevButton.setEnabled(False) | |
self.playButton.setEnabled(False) | |
self.nextButton.setEnabled(False) | |
button_layout = QHBoxLayout() | |
button_layout.addWidget(self.prevButton) | |
button_layout.addWidget(self.playButton) | |
button_layout.addWidget(self.nextButton) | |
main_layout = QVBoxLayout() | |
main_layout.addWidget(self.imageLabel) | |
main_layout.addLayout(button_layout) | |
self.setLayout(main_layout) | |
self.prevButton.clicked.connect(self.on_prev) | |
self.nextButton.clicked.connect(self.on_next) | |
self.playButton.clicked.connect(self.on_play) | |
self.timer = QTimer(self) | |
self.timer.setInterval(33) | |
self.timer.timeout.connect(self.on_next) | |
self.frames = [] | |
self.current_frame_index = -1 | |
self.ffmpeg_process = None | |
self.width = None | |
self.height = None | |
def load_video(self, file_path): | |
if self.ffmpeg_process: | |
self.ffmpeg_process.kill() | |
self.ffmpeg_process = None | |
width, height = get_video_info(file_path) | |
if width is None or height is None: | |
self.imageLabel.setText("Failed to get video info") | |
return | |
self.width, self.height = width, height | |
self.frames = [] | |
self.current_frame_index = -1 | |
cmd = [ | |
"ffmpeg", | |
"-vcodec", "libvpx", | |
"-i", file_path, | |
"-f", "rawvideo", | |
"-pix_fmt", "rgba", | |
"-hide_banner", | |
"-loglevel", "error", | |
"pipe:1", | |
] | |
self.ffmpeg_process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) | |
self.nextButton.setEnabled(True) | |
self.prevButton.setEnabled(False) | |
self.playButton.setEnabled(True) | |
self.playButton.setText("Play") | |
if self.timer.isActive(): | |
self.timer.stop() | |
self.on_next() | |
def read_next_frame(self): | |
frame_size = self.width * self.height * 4 | |
raw_frame = self.ffmpeg_process.stdout.read(frame_size) | |
if len(raw_frame) < frame_size: | |
return None | |
frame = np.frombuffer(raw_frame, np.uint8).reshape((self.height, self.width, 4)) | |
return frame | |
def show_frame(self, frame): | |
bytes_per_line = self.width * 4 | |
qimage = QImage(frame.data, self.width, self.height, bytes_per_line, QImage.Format.Format_RGBA8888).copy() | |
pixmap = QPixmap.fromImage(qimage) | |
self.imageLabel.setPixmap(pixmap) | |
self.imageLabel.setFixedSize(pixmap.size()) | |
def on_next(self): | |
if self.current_frame_index + 1 < len(self.frames): | |
self.current_frame_index += 1 | |
self.show_frame(self.frames[self.current_frame_index]) | |
else: | |
frame = self.read_next_frame() | |
if frame is None: | |
if self.timer.isActive(): | |
self.timer.stop() | |
self.playButton.setText("Play") | |
self.nextButton.setEnabled(False) | |
print("Reached the end of the video.") | |
return | |
self.frames.append(frame) | |
self.current_frame_index += 1 | |
self.show_frame(frame) | |
if self.current_frame_index > 0: | |
self.prevButton.setEnabled(True) | |
def on_prev(self): | |
if self.timer.isActive(): | |
self.timer.stop() | |
self.playButton.setText("Play") | |
if self.current_frame_index > 0: | |
self.current_frame_index -= 1 | |
self.show_frame(self.frames[self.current_frame_index]) | |
if self.current_frame_index == 0: | |
self.prevButton.setEnabled(False) | |
def on_play(self): | |
if self.timer.isActive(): | |
self.timer.stop() | |
self.playButton.setText("Play") | |
else: | |
self.timer.start() | |
self.playButton.setText("Stop") | |
class MainWindow(QMainWindow): | |
def __init__(self): | |
super().__init__() | |
self.setWindowTitle("WebM Frame Viewer (FFmpeg)") | |
self.player = VideoPlayerWidget() | |
self.loadButton = QPushButton("Load Video") | |
self.loadButton.clicked.connect(self.open_file) | |
central_widget = QWidget() | |
layout = QVBoxLayout(central_widget) | |
layout.addWidget(self.loadButton) | |
layout.addWidget(self.player) | |
self.setCentralWidget(central_widget) | |
def open_file(self): | |
file_path, _ = QFileDialog.getOpenFileName( | |
self, | |
"Open WebM Video", | |
"", | |
"WebM Videos (*.webm);;All Files (*)" | |
) | |
if file_path: | |
self.player.load_video(file_path) | |
def main(): | |
app = QApplication(sys.argv) | |
window = MainWindow() | |
window.show() | |
sys.exit(app.exec()) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Performance on this isn't for playing long webms but we can step frame by frame through webm's
Learn: https://stackoverflow.com/questions/22485022/splitting-webm-video-to-png-with-transparency