Tkinterで動画を再生する最低限の方法

#概要
pythonでGUIアプリが作れるtkinterに動画を再生する方法をまとめた記事です
openCVを使うため、音声の出力はないです

完成形は最低限(動画の表示と再生,停止の制御のみ)ですのでこんな感じです
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1612817/2b70b6ed-6eff-51ce-9b9b-e852f877a3b6.png)
python 3.8.3
opencv-python 4.4.0.44
pillow 7.2.0

#ソース
```python:VideoPlayer.py
import cv2
from PIL import Image, ImageTk
import tkinter as tk
from tkinter import messagebox
import threading as th

lock = th.Lock()

class VideoPlayer(tk.Frame):
    def __init__(self,master=None):
        super().__init__(master,width=1000,height=500)
        master.minsize(width=1000,height=500)
        self.config(bg="#000000")
        self.pack(expand=True,fill=tk.BOTH)
        self.video = None
        self.playing = False
        self.video_thread = None
        self.create_video_button()

    def create_video_button(self):
        self.video_button = tk.Button(
            self,
            width = 100,
            height = 25,
            bg="#DDDDDD",
            relief="flat",
            command = self.push_play_button,
        )
        self.video_button.pack(expand=True,fill=tk.BOTH)

    def get_video(self,path):
        self.video = cv2.VideoCapture(path)

    def push_play_button(self):
        if self.video == None:
            messagebox.showerror('エラー','動画データがありません')
            return

        self.playing = not self.playing
        if self.playing:
            self.video_thread = th.Thread(target=self.video_frame_timer)
            self.video_thread.setDaemon(True)
            self.video_thread.start()            
        else:
            self.video_thread = None 

    def next_frame(self):
        global lock
        lock.acquire()
        ret, self.frame = self.video.read()
        if not ret:
            messagebox.showerror("エラー","次のフレームがないので停止します")
            self.playing = False
        else:
            rgb = cv2.cvtColor(self.frame, cv2.COLOR_BGR2RGB)
            pil = Image.fromarray(rgb)
            x = self.video_button.winfo_width()/pil.width
            y = self.video_button.winfo_height()/pil.height
            ratio = x if x<y else y #三項演算子 xとyを比較して小さい方を代入
            pil = pil.resize((int(ratio*pil.width),int(ratio*pil.height)))
            image = ImageTk.PhotoImage(pil)
            self.video_button.config(image=image)
            self.video_button.image = image
        lock.release()

    def video_frame_timer(self):
        while self.playing:
            self.next_frame()

if __name__ == "__main__":
    root = tk.Tk()
    path = "*.mp4"
    app = VideoPlayer(master=root)
    app.get_video(path)
    root.mainloop()
```
画面をクリックするだけで、動画が再生される

#なぜthreadなのか
tkinterにはafter()と呼ばれる、数秒間隔で実行してくれる関数がありますが、
問題点が複数あります

- 約数秒間隔
- 重い処理が挟まると再生も遅くなる

実はafter()は正確な秒数間隔で実行が行われていないみたいです
また、メインの中で重い処理を行うと、伴って動画の再生もスローになってしまいます

その点threadを使うことで、動画の再生はサブプロセスで動き、メインの処理の負荷を無視できます
###主に関係のあるソース
```python:thread
 def push_play_button(self):
        self.playing = not self.playing
        if self.playing:
            self.video_thread = th.Thread(target=self.video_frame_timer)
            self.video_thread.setDaemon(True)
            self.video_thread.start()            
        else:
            self.video_thread = None 

 def video_frame_timer(self):
        while self.playing:
            self.next_frame()
```
画面をクリックしたときthreadを作成し、
画面の更新はwhileで実行している
threadはdaemonにすることで、画面を閉じたときに勝手に終了してくれる
threadは上書きされるため、threadは常にメインとサブの2つのみ
#canvasを使わない理由
先駆者様は動画を再生するためにcanvasを使っていますが、
処理が重いため、画面が白くちらつきました(処理が描画に間に合ってない)

labelやbuttonを用意して、イメージを更新することで同じことができ、
処理も軽いのでよっぽどの理由がない限りcanvasで作る必要はないと思います
(buttonで作ると再生ボタンも兼ねられるので個人的に好き)
#まとめ
tkinter cpenCVで動画処理の記事はいっぱいありますが、
threadを使って動画データを処理する記事が思った以上になかったです
拙い記事ですが、ないよりましの精神で書きましたので、少しでも参考になれば幸いです