Skip to content

Instantly share code, notes, and snippets.

@downthecrop
Created February 12, 2025 08:04
Show Gist options
  • Save downthecrop/9ca8ea0cda6c14d4eb5c962b410c9a94 to your computer and use it in GitHub Desktop.
Save downthecrop/9ca8ea0cda6c14d4eb5c962b410c9a94 to your computer and use it in GitHub Desktop.
PyQt6 WebM Player With Transparency
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()
@downthecrop
Copy link
Author

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment