はじめに
Pythonの標準ライブラリであるtkinterで動画を再生する方法とそれを使ったアプリの作り方を紹介します。
この記事の対象者
- tkinterについての基本的な知識があり、ウィンドウで動画を再生したい。
前提知識
- 基本的なPythonの記法と仕様を理解している
- tkinter, opencv-python, pillowの基本的な使い方を知っている
実装
必要なライブラリのインストール
この記事ではOpenCV(opencv-python)とPillowを使った方法を紹介するので、それをインストールします。
pip install -U opencv-python pillow
実際のコード
先にコードの全体を紹介します。
import tkinter as tk
from tkinter import ttk
import cv2
from PIL import Image, ImageTk
class VideoPlayer:
def __init__(self, window, video_source=0):
self.window = window
self.window.title("Video Player")
self.video_source = video_source
self.vid = cv2.VideoCapture(self.video_source)
self.canvas = tk.Canvas(window)
self.canvas.grid(row=0, column=0, columnspan=5)
self.btn_rewind = tk.Button(window, text="<< 5s", width=10, command=self.rewind)
self.btn_rewind.grid(row=1, column=0, sticky="ew")
self.btn_play_pause = tk.Button(window, text="Play", width=10, command=self.toggle_play)
self.btn_play_pause.grid(row=1, column=1, sticky="ew")
self.btn_skip = tk.Button(window, text="5s >>", width=10, command=self.skip)
self.btn_skip.grid(row=1, column=2, sticky="ew")
self.progress_bar = ttk.Progressbar(window, orient="horizontal", length=200, mode="determinate")
self.progress_bar.grid(row=1, column=3, sticky="ew")
self.lbl_timestamp = tk.Label(window, text="00:00/00:00")
self.lbl_timestamp.grid(row=1, column=4, sticky="ew")
self.paused = True
self.update()
def toggle_play(self):
self.paused = not self.paused
if self.paused:
self.btn_play_pause.config(text="Play")
else:
self.btn_play_pause.config(text="Pause")
self.update()
def rewind(self):
current_frame = int(self.vid.get(cv2.CAP_PROP_POS_FRAMES))
fps = self.vid.get(cv2.CAP_PROP_FPS)
target_frame = max(current_frame - 5 * fps, 0)
self.vid.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
def skip(self):
current_frame = int(self.vid.get(cv2.CAP_PROP_POS_FRAMES))
total_frames = int(self.vid.get(cv2.CAP_PROP_FRAME_COUNT))
fps = self.vid.get(cv2.CAP_PROP_FPS)
target_frame = min(current_frame + 5 * fps, total_frames)
self.vid.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
def update(self):
if not self.paused:
ret, frame = self.vid.read()
if ret:
self.photo = ImageTk.PhotoImage(image=Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)))
self.canvas.config(width=self.vid.get(cv2.CAP_PROP_FRAME_WIDTH), height=self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT))
self.canvas.create_image(0, 0, image=self.photo, anchor=tk.NW)
current_frame = int(self.vid.get(cv2.CAP_PROP_POS_FRAMES))
total_frames = int(self.vid.get(cv2.CAP_PROP_FRAME_COUNT))
self.progress_bar["value"] = (current_frame / total_frames) * 100
current_time = int(self.vid.get(cv2.CAP_PROP_POS_MSEC) / 1000)
total_time = int(total_frames / self.vid.get(cv2.CAP_PROP_FPS))
current_time_str = self.format_time(current_time)
total_time_str = self.format_time(total_time)
self.lbl_timestamp.config(text=f"{current_time_str}/{total_time_str}")
fps = self.vid.get(cv2.CAP_PROP_FPS)
delay = int(1000 / fps)
self.window.after(delay, self.update)
else:
self.toggle_play()
else:
self.btn_play_pause.config(text="Play")
def format_time(self, seconds):
hours = seconds // 3600
minutes = (seconds % 3600) // 60
seconds = seconds % 60
if hours > 0:
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
else:
return f"{minutes:02d}:{seconds:02d}"
root = tk.Tk()
player = VideoPlayer(root, "your_video_file.mp4")
root.mainloop()
解説
VideoPlayer
今回作成するアプリケーションのクラスです。
__init__(self, window, video_source=0)
このクラスのコンストラクタ(初期化メソッド)です。window
はTkinterの親ウィンドウのインスタンスを受け取り、video_source
はビデオソースのパスまたはデバイス番号 (デフォルトは0
=デフォルトカメラ)を受け取ります。
self.window = window
self.window.title("Video Player")
受け取ったwindow
をself.window
に割り当て、ウィンドウのタイトルを"Video Player"
に設定します。
self.video_source = video_source
self.vid = cv2.VideoCapture(self.video_source)
video_source
をself.video_source
に割り当て、cv2.VideoCapture
でself.vid
を作成します。これによってビデオキャプチャオブジェクトを初期化します。
self.canvas = tk.Canvas(window)
self.canvas.grid(row=0, column=0, columnspan=5)
self.canvas
はTkinterのCanvas
ウィジェットを作成し、それをウィンドウのグリッドレイアウトに配置します。columnspan=5
は、このCanvas
が5
つのカラムにまたがることを意味します。
self.btn_rewind = tk.Button(window, text="<< 5s", width=10, command=self.rewind)
self.btn_rewind.grid(row=1, column=0, sticky="ew")
self.btn_rewind
は、"<< 5s"
というテキストを持つ幅10
のButton
ウィジェットを作成します。command=self.rewind
は、このボタンがクリックされたときにself.rewind()
メソッドが呼び出されることを意味します。最後に、このボタンをグリッドレイアウトに配置しています。
self.btn_play_pause = tk.Button(window, text="Play", width=10, command=self.toggle_play)
self.btn_play_pause.grid(row=1, column=1, sticky="ew")
self.btn_play_pause
は、"Play"
というテキストを持つ幅10
のButton
ウィジェットを作成します。command=self.toggle_play
は、このボタンがクリックされたときにself.toggle_play()
メソッドが呼び出されることを意味します。最後に、このボタンをグリッドレイアウトに配置します。
self.btn_skip = tk.Button(window, text="5s >>", width=10, command=self.skip)
self.btn_skip.grid(row=1, column=2, sticky="ew")
self.btn_skip
は、"5s >>"
というテキストを持つ幅10
のButton
ウィジェットを作成します。command=self.skip
は、このボタンがクリックされたときにself.skip()
メソッドが呼び出されることを意味します。最後に、このボタンをグリッドレイアウトに配置します。
self.progress_bar = ttk.Progressbar(window, orient="horizontal", length=200, mode="determinate")
self.progress_bar.grid(row=1, column=3, sticky="ew")
self.progress_bar
は、水平方向の長さ200
のttk.Progressbar
ウィジェットを作成します。mode="determinate"
は、プログレスバーが確定的な値を表示することを意味します。最後に、このウィジェットをグリッドレイアウトに配置します。
self.lbl_timestamp = tk.Label(window, text="00:00/00:00")
self.lbl_timestamp.grid(row=1, column=4, sticky="ew")
self.lbl_timestamp
は、初期テキスト"00:00/00:00"
を持つtk.Label
ウィジェットを作成します。これは、現在の時間と総時間を表示するために使用されます。最後に、このウィジェットをグリッドレイアウトに配置します。
self.paused = True
self.update()
-
self.paused
は、初期状態でTrue
に設定されます。これは、プレイヤーが最初は一時停止状態であることを意味します。 -
self.update()
メソッドが呼び出されます。このメソッドは後で説明します。
toggle_play(self)
def toggle_play(self):
self.paused = not self.paused
if self.paused:
self.btn_play_pause.config(text="Play")
else:
self.btn_play_pause.config(text="Pause")
self.update()
このメソッドはself.paused
の値を反転させます(True
をFalse
に、False
をTrue
に)。
self.paused
がTrue
の場合、self.btn_play_pause
のテキストを"Play"
に設定します。False
の場合は"Pause"
に設定します。
最後に、self.update()
メソッドを呼び出します。
rewind(self)
def rewind(self):
current_frame = int(self.vid.get(cv2.CAP_PROP_POS_FRAMES))
fps = self.vid.get(cv2.CAP_PROP_FPS)
target_frame = max(current_frame - 5 * fps, 0)
self.vid.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
このメソッドは、動画を5
秒巻き戻します。
current_frame
は現在のフレーム番号を取得します。
fps
は動画のフレームレートを取得します。
target_frame
は目標のフレーム番号を計算します。current_frame - 5 * fps
で5秒前のフレームを計算し、max(_, 0)
で0未満にならないようにしています。
self.vid.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
でビデオキャプチャの現在のフレーム位置をtarget_frame
に設定します。
skip(self)
def skip(self):
current_frame = int(self.vid.get(cv2.CAP_PROP_POS_FRAMES))
total_frames = int(self.vid.get(cv2.CAP_PROP_FRAME_COUNT))
fps = self.vid.get(cv2.CAP_PROP_FPS)
target_frame = min(current_frame + 5 * fps, total_frames)
self.vid.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
このメソッドは動画を5
秒早送りします。
current_frame
とtotal_frames
は現在のフレーム番号と総フレーム数を取得します。
fps
は、動画のフレームレートを取得します。
target_frame
は目標のフレーム番号を計算します。current_frame + 5 * fps
で5秒後のフレームを計算し、min(_, total_frames)
で総フレーム数を超えないようにしています。
self.vid.set(cv2.CAP_PROP_POS_FRAMES, target_frame)
でビデオキャプチャの現在のフレーム位置をtarget_frame
に設定します。
update(self)
def update(self):
if not self.paused:
ret, frame = self.vid.read()
if ret:
self.photo = ImageTk.PhotoImage(image=Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)))
self.canvas.config(width=self.vid.get(cv2.CAP_PROP_FRAME_WIDTH), height=self.vid.get(cv2.CAP_PROP_FRAME_HEIGHT))
self.canvas.create_image(0, 0, image=self.photo, anchor=tk.NW)
current_frame = int(self.vid.get(cv2.CAP_PROP_POS_FRAMES))
total_frames = int(self.vid.get(cv2.CAP_PROP_FRAME_COUNT))
self.progress_bar["value"] = (current_frame / total_frames) * 100
current_time = int(self.vid.get(cv2.CAP_PROP_POS_MSEC) / 1000)
total_time = int(total_frames / self.vid.get(cv2.CAP_PROP_FPS))
current_time_str = self.format_time(current_time)
total_time_str = self.format_time(total_time)
self.lbl_timestamp.config(text=f"{current_time_str}/{total_time_str}")
fps = self.vid.get(cv2.CAP_PROP_FPS)
delay = int(1000 / fps)
self.window.after(delay, self.update)
else:
self.toggle_play()
else:
self.btn_play_pause.config(text="Play")
このメソッドは、動画の再生とGUIの更新を行います。
if not self.paused:
で一時停止状態でない場合に以下の処理を行います。
ret, frame = self.vid.read()
で次のフレームを読み込みます。ret
は読み込みの成功/失敗を示すブール値です。
ret
がTrue
の場合:
self.photo
に、フレームをTkinter用の画像オブジェクトに変換したものを設定します。
self.canvas
の幅と高さをフレームのサイズに合わせて設定します。
self.canvas
にself.photo
画像を表示します。
current_frame
とtotal_frames
を取得し、self.progress_bar["value"]
にその割合を設定します。
current_time
とtotal_time
(秒数)を計算し、self.format_time
関数を使って文字列をフォーマットします。
self.lbl_timestamp
にその時間文字列を設定します。
fps
を取得し、次のフレームの遅延時間delay
を計算します。
self.window.after(delay, self.update)
でdelay
ミリ秒後にself.update
を再び呼び出すことで、動画が正常な速度で再生されます。
ret
がFalse
の場合:
self.toggle_play()
を呼び出して再生を停止します。
if not self.paused:
がelse
節を実行した場合、つまり一時停止状態の場合はself.btn_play_pause
のテキストを"Play"
に設定します。
format_time(self)
def format_time(self, seconds):
hours = seconds // 3600
minutes = (seconds % 3600) // 60
seconds = seconds % 60
if hours > 0:
return f"{hours:02d}:{minutes:02d}:{seconds:02d}"
else:
return f"{minutes:02d}:{seconds:02d}"
このメソッドは秒数を"HH:MM:SS"
形式の文字列に変換します。
hours
、minutes
、seconds
を計算します。
hours
が0
より大きい場合は"HH:MM:SS"
形式で返します。そうでない場合は"MM:SS"
形式で返します。
実行部分
root = tk.Tk()
player = VideoPlayer(root, "your_video_file.mp4")
root.mainloop()
root = tk.Tk()
で、Tkinterの親ウィンドウを作成します。
player = VideoPlayer(root, "your_video_file.mp4")
でVideoPlayer
クラスのインスタンスを作成します。root
をウィンドウとして渡し、"your_video_file.mp4"
を動画ソースとして渡します。
root.mainloop()
でTkinterのメインループを開始し、ウィンドウが閉じられるまでプログラムが実行され続けます。
終わりに
最後まで読んで頂きありがとうございます。不備があれば是非コメントで指摘して下さい。