81
91

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Python】Tkinterで簡易的な動画プレイヤーを作る

Last updated at Posted at 2024-04-13

はじめに

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")

受け取ったwindowself.windowに割り当て、ウィンドウのタイトルを"Video Player"に設定します。

self.video_source = video_source
self.vid = cv2.VideoCapture(self.video_source)

video_sourceself.video_sourceに割り当て、cv2.VideoCaptureself.vidを作成します。これによってビデオキャプチャオブジェクトを初期化します。

self.canvas = tk.Canvas(window)
self.canvas.grid(row=0, column=0, columnspan=5)

self.canvasはTkinterのCanvasウィジェットを作成し、それをウィンドウのグリッドレイアウトに配置します。columnspan=5は、このCanvas5つのカラムにまたがることを意味します。

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"というテキストを持つ幅10Buttonウィジェットを作成します。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"というテキストを持つ幅10Buttonウィジェットを作成します。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 >>"というテキストを持つ幅10Buttonウィジェットを作成します。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は、水平方向の長さ200ttk.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()
  1. self.pausedは、初期状態でTrueに設定されます。これは、プレイヤーが最初は一時停止状態であることを意味します。
  2. 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の値を反転させます(TrueFalseに、FalseTrueに)。
self.pausedTrueの場合、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_frametotal_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は読み込みの成功/失敗を示すブール値です。

retTrueの場合:
self.photoに、フレームをTkinter用の画像オブジェクトに変換したものを設定します。
self.canvasの幅と高さをフレームのサイズに合わせて設定します。
self.canvasself.photo画像を表示します。
current_frametotal_framesを取得し、self.progress_bar["value"]にその割合を設定します。
current_timetotal_time(秒数)を計算し、self.format_time関数を使って文字列をフォーマットします。
self.lbl_timestampにその時間文字列を設定します。
fpsを取得し、次のフレームの遅延時間delayを計算します。
self.window.after(delay, self.update)delayミリ秒後にself.updateを再び呼び出すことで、動画が正常な速度で再生されます。

retFalseの場合:
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"形式の文字列に変換します。
hoursminutessecondsを計算します。
hours0より大きい場合は"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のメインループを開始し、ウィンドウが閉じられるまでプログラムが実行され続けます。

終わりに

最後まで読んで頂きありがとうございます。不備があれば是非コメントで指摘して下さい。

81
91
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
81
91

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?