PyQt6를 이용한 동영상플레이어앱


개요


안녕하세요.

예전에 PyQt5 기반의 동영상플레이어를 만든 게시물이 있었습니다.

수업 때 종종 활용해왔던 코드인데, 2024년부터 학원수업에 Qt6기반의 PyQt6를 사용하다 보니 PyQt5와 다른 몇가지 변화가 있어 새롭게 동영상 플레이어를 만들어 보았습니다. 


PyQt6 Multimedia 변경사항


새롭게 추가된 부분을 보면 레코딩 관련 클래스들이 추가되었습니다.

출처 : Qt Documentation

삭제된 부분은 추가보다 더 많습니다.

존재의 의미를 잘 몰랐던 QMediaContent (QUrl과 다른점이 🤔 )가 삭제되었으며, 잘 사용해오던 QMediaPlaylist가 삭제되었습니다. 😩

출처 : Qt Documentation

이제 미디어 파일(비디오, 오디오 등)을 리스트로 관리해주던 QMediaPlaylist 클래스가 없어지면서 사용자가 직접 미디어 파일들을 관리해야 합니다.

본 예제에서는 파이썬의 리스트를 사용해서 처리하였습니다.

Qt는 크로스 플랫폼을 지원하기 때문에 그 클래스 내부는 다양한 OS에 대응하기 위해 복잡한 구조로 이루어져 있을 것으로 추측되고 편의적인 목적의 클래스들이 삭제된 것으로 추측됩니다.

좀 더 세부적인 변화는 아래 Qt 링크를 참조하기 바랍니다.

Changes to Qt Multimedia


개발환경


  • Windows 11 Pro, MS Visual Studio 2022 

  • Python 3.11.9 64bit, PyQt6 6.7.0

 


소스코드


Git Link : MoviePlayer

소스코드는 main.py, multimedia.py 2개로 구성되어 있으며, 모든 소스코드 및 Qt Designer로 작성된 *.ui 파일은 git link에 포함되어 있습니다.

Qt Designer에서 QVideoWidget 생성시 일반 Widget으로 추가하고 아래 그림처럼 승격하면 됩니다.

 

Widget to QVideoWidget

main.py 소스코드

from PyQt6.QtWidgets import QApplication, QWidget, QFileDialog
from PyQt6.uic import loadUi
from multimedia import Media
import sys

class Window(QWidget):

    def __init__(self):
        super().__init__()
        loadUi('form.ui', self)        
        self.setWindowTitle('Ocean Coding School')

        self.media = Media(self)
        self.dial.setRange(0,100)
        self.dial.setValue(50)

        self.playtime = ''

        # signals
        self.pb_add.clicked.connect(self.onAdd)
        self.pb_del.clicked.connect(self.onDel)
        
        self.pb_play.clicked.connect(self.onPlay)
        self.pb_stop.clicked.connect(self.onStop)
        self.pb_pau.clicked.connect(self.onPause)
        self.pb_ff.clicked.connect(self.onFF)
        self.pb_prev.clicked.connect(self.onPrev)

        self.dial.valueChanged.connect(self.onDial)
        self.lw.itemDoubleClicked.connect(self.onDbClick)

    def onAdd(self):
        path = QFileDialog.getOpenFileNames(self, '', '', '(Media Files (*.mp3 *.mp4 *.mkv *.avi *.mov)')
        cnt = self.lw.count()

        for file in path[0]:
            lst = file.split('/')
            self.lw.addItem(lst[-1])

        self.media.addMedia(path[0])            

        if cnt==0:
            self.lw.setCurrentRow(0)

    def onDel(self):
        row = self.lw.currentRow()
        self.lw.takeItem(row)
        self.media.delMedia(row)

    def onPlay(self):
        row = self.lw.currentRow()
        self.media.playMedia(row)

    def onStop(self):
        self.media.stopMedia()

    def onPause(self):
        self.media.pauseMedia()

    def onFF(self):
        row = self.lw.currentRow()+1
        if row==self.lw.count():
            row = 0
        self.lw.setCurrentRow(row)
        self.media.ffMedia(row)

    def onPrev(self):
        row = self.lw.currentRow()-1
        if row<0:
            row = self.lw.count()-1
        self.lw.setCurrentRow(row)
        self.media.prevMedia(row)

    def onDial(self):
        val = self.dial.value()
        self.media.volumeMedia(val)

    def onDbClick(self, item):
        self.onPlay()

    def onDC(self, t):        
        self.hsld.setRange(0, t)
        h, m, s = self.hmsFromSecond(t//1000)
        self.playtime = f'{h:02}:{m:02}:{s:02}'

    def onPC(self, pos):
        self.hsld.setValue(pos)        
        h, m, s = self.hmsFromSecond(pos//1000)
        currtime = f'{h:02}:{m:02}:{s:02}'
        self.lb.setText(f'{currtime} / {self.playtime}')        

    def hmsFromSecond(self, sec):
        h = sec//3600
        m = (sec%3600)//60
        s = (sec%3600)%60
        return h, m, s

if __name__ == '__main__':
    app = QApplication(sys.argv)
    w = Window()
    w.show()
    sys.exit(app.exec())
    

코드작성은 제가 직접 진행하였으며, 아래 코드분석은 ChatGPT 4Go 에서 작성, 제가 검수하였습니다.

Window 클래스는 PyQt6를 사용하여 미디어 플레이어의 GUI를 설정하고, 다양한 사용자 동작(이벤트)에 대해 정의된 기능을 수행하는 메인 애플리케이션 창을 구성합니다. 이 클래스는 사용자 인터페이스의 버튼 클릭, 더블 클릭, 슬라이더 조정 등의 이벤트와 연결된 여러 메서드를 포함합니다.

Window 클래스 함수별 분석

1. __init__(self)

Window 클래스의 초기화 메서드로, UI를 설정하고 초기 값을 할당하며, 이벤트와 슬롯을 연결합니다.

loadUi('form.ui', self): form.ui 파일을 로드하여 UI 레이아웃과 위젯을 설정합니다.

self.setWindowTitle('Ocean Coding School'): 창의 제목을 "Ocean Coding School"으로 설정합니다.

self.media = Media(self): Media 클래스의 인스턴스를 생성하여 미디어 관련 기능을 초기화합니다.

self.dial.setRange(0, 100) 및 self.dial.setValue(50): 볼륨을 제어하는 다이얼의 범위를 0에서 100으로 설정하고, 초기값을 50으로 설정합니다.

여러 버튼과 위젯의 신호(signal)를 각 메서드(slot)와 연결합니다. 예를 들어, self.pb_add.clicked.connect(self.onAdd)는 "추가" 버튼을 클릭할 때 onAdd 메서드를 호출합니다.

2. onAdd(self)

사용자가 미디어 파일을 추가하려고 할 때 호출됩니다.

QFileDialog.getOpenFileNames를 사용하여 파일 선택 대화 상자를 열고, 사용자가 선택한 미디어 파일의 경로를 가져옵니다.

self.lw(리스트 위젯)에 각 파일의 이름을 추가합니다.

self.media.addMedia(path[0])를 호출하여 선택된 파일들을 Media 객체에 추가합니다.

리스트가 비어 있을 경우, 첫 번째 항목을 현재 선택된 항목으로 설정합니다.

3. onDel(self)

사용자가 선택한 미디어 파일을 삭제하려고 할 때 호출됩니다.

현재 선택된 항목의 인덱스를 가져와 리스트 위젯에서 해당 항목을 제거합니다.

self.media.delMedia(row)를 호출하여 Media 객체에서도 해당 파일을 삭제합니다.

4. onPlay(self)

"재생" 버튼이 클릭되었을 때 호출됩니다.

현재 선택된 항목의 인덱스를 가져와 self.media.playMedia(row)를 호출하여 미디어 파일을 재생합니다.

5. onStop(self)

"정지" 버튼이 클릭되었을 때 호출됩니다.

self.media.stopMedia()를 호출하여 현재 재생 중인 미디어를 정지합니다.

6. onPause(self)

"일시 정지" 버튼이 클릭되었을 때 호출됩니다.

self.media.pauseMedia()를 호출하여 현재 재생 중인 미디어를 일시 정지합니다.

7. onFF(self)
"다음" 버튼이 클릭되었을 때 호출됩니다.

현재 선택된 항목의 인덱스를 가져와 다음 인덱스로 설정합니다. 인덱스가 마지막 항목을 넘어가면 0으로 설정하여 처음으로 돌아갑니다.

self.media.ffMedia(row)를 호출하여 다음 미디어 파일을 재생합니다.

8. onPrev(self) 

"이전" 버튼이 클릭되었을 때 호출됩니다.

현재 선택된 항목의 인덱스를 가져와 이전 인덱스로 설정합니다.

인덱스가 첫 번째 항목보다 작아지면 마지막 항목으로 설정하여 끝으로 돌아갑니다.

self.media.prevMedia(row)를 호출하여 이전 미디어 파일을 재생합니다.

9. onDial(self)

볼륨 다이얼이 변경되었을 때 호출됩니다.다이얼의 현재 값을 가져와 self.media.volumeMedia(val) 을 호출하여 오디오의 볼륨을 조정합니다.

10. onDbClick(self, item)

리스트 위젯에서 항목을 더블 클릭했을 때 호출됩니다.

더블 클릭된 항목을 재생하기 위해 onPlay() 메서드를 호출합니다.

11. onDC(self, t)

미디어의 전체 재생 시간이 변경되었을 때 호출됩니다.

슬라이더의 범위를 재생 시간으로 설정하고, 시간을 시, 분, 초 단위로 변환하여 self.playtime 변수에 저장합니다.

12. onPC(self, pos)

현재 재생 위치가 변경되었을 때 호출됩니다.

슬라이더의 값을 현재 재생 위치로 설정하고, 시간을 시, 분, 초 단위로 변환하여 레이블(self.lb)에 표시합니다.

13. hmsFromSecond(self, sec)

초 단위의 시간을 시, 분, 초로 변환합니다.

초 단위(sec)의 시간을 시(h), 분(m), 초(s)로 변환하여 반환합니다.

요약

Window 클래스는 미디어 파일의 재생, 일시정지, 정지, 볼륨 조정, 파일 추가 및 삭제 등 다양한 기능을 수행하도록 정의되어 있으며, 사용자 인터페이스와의 상호작용을 관리하는 역할을 합니다. 각 메서드는 사용자 동작(이벤트)에 대응하여 미디어 플레이어의 상태를 변경하거나, Media 객체를 통해 해당 동작을 수행하도록 합니다. 


multimedia.py 소스코드

from PyQt6.QtMultimedia import QMediaPlayer, QAudioOutput
from PyQt6.QtCore import QObject, QUrl, pyqtSignal

class Media(QObject):

    dc_signal = pyqtSignal(int)
    pc_signal = pyqtSignal(int)

    def __init__(self, w):
        super().__init__()
        self.parent = w

        self.mp = QMediaPlayer()
        self.ao = QAudioOutput()
        self.ao.setVolume(0.5)
        self.pl = []

        self.mp.setAudioOutput(self.ao)
        self.mp.setVideoOutput(w.vw)

        # signals
        self.mp.durationChanged.connect(self.onDC)
        self.mp.positionChanged.connect(self.onPC)

        # emit signals to widget        
        self.dc_signal.connect(self.parent.onDC)
        self.pc_signal.connect(self.parent.onPC)

    def addMedia(self, files):
        for file in files:            
            url = QUrl.fromLocalFile(file)
            self.pl.append(url)

    def delMedia(self, row):
        del(self.pl[row])        

    def playMedia(self, idx=0):        
        if self.pl:
            if self.mp.playbackState() == QMediaPlayer.PlaybackState.StoppedState:
                self.mp.setSource( self.pl[idx] )
            elif self.mp.playbackState() == QMediaPlayer.PlaybackState.PausedState:
                pass
            self.mp.play()

    def stopMedia(self):
        self.mp.stop()

    def pauseMedia(self):
        self.mp.pause()

    def ffMedia(self, idx=0):
        self.stopMedia()
        self.playMedia(idx)

    def prevMedia(self, idx=0):
        self.stopMedia()
        self.playMedia(idx)

    def volumeMedia(self, vol):
        self.ao.setVolume(vol/100)

    def onDC(self, t):        
        self.dc_signal.emit(t)
        
    def onPC(self, pos):        
        self.pc_signal.emit(pos)
        

Media 클래스 함수별 분석

1. __init__(self, w)

 Media 클래스의 초기화 메서드로, QMediaPlayer와 QAudioOutput 객체를 생성하고, 부모 위젯 w와 연결합니다.

QMediaPlayer 객체를 self.mp에 할당하고, QAudioOutput 객체를 self.ao에 할당합니다.

self.ao의 초기 볼륨을 0.5로 설정합니다.

self.pl은 미디어 파일 경로를 저장할 리스트입니다.

self.mp의 오디오 및 비디오 출력을 설정합니다.

미디어의 재생 시간(durationChanged)과 현재 위치(positionChanged)에 대한 시그널을 연결하여, 시간이 변경될 때마다 onDC 및 onPC 메서드가 호출되도록 합니다.

2. addMedia(self, files)

미디어 파일을 추가하는 메서드입니다.

files 리스트에 있는 각 파일의 경로를 QUrl 객체로 변환한 후 self.pl 리스트에 추가합니다.

3. delMedia(self, row)

지정된 행에 해당하는 미디어 파일을 삭제합니다.

self.pl 리스트에서 row에 해당하는 인덱스의 항목을 삭제합니다.

4. playMedia(self, idx=0)

지정된 인덱스에 있는 미디어 파일을 재생합니다.

self.pl에 파일이 존재하면 재생 상태를 확인한 후, 미디어 파일을 재생합니다.

정지된 상태에서는 self.mp에 미디어 소스를 설정하고 재생합니다.

일시정지 상태에서는 재생을 계속합니다.

5. stopMedia(self)

현재 재생 중인 미디어를 정지합니다.

self.mp의 stop() 메서드를 호출하여 미디어 재생을 중단합니다.

6. pauseMedia(self)

현재 재생 중인 미디어를 일시정지합니다.

self.mp의 pause() 메서드를 호출하여 미디어 재생을 일시 정지합니다.

7. ffMedia(self, idx=0)

다음 미디어 파일로 빠르게 이동하여 재생합니다.

stopMedia() 메서드를 호출하여 현재 재생 중인 미디어를 정지한 후, playMedia(idx)를 호출하여 지정된 인덱스의 미디어를 재생합니다.

8. prevMedia(self, idx=0)

이전 미디어 파일로 이동하여 재생합니다.

stopMedia() 메서드를 호출하여 현재 재생 중인 미디어를 정지한 후, playMedia(idx)를 호출하여 지정된 인덱스의 미디어를 재생합니다.

9. volumeMedia(self, vol)

오디오 출력의 볼륨을 조정합니다.

vol 매개변수를 100으로 나눈 값을 self.ao의 setVolume 메서드에 전달하여 볼륨을 설정합니다.

10. onDC(self, t)

미디어의 전체 재생 시간을 전달하는 시그널을 방출합니다.

미디어 재생 시간(t)을 dc_signal 시그널로 방출하여 UI에 표시되도록 합니다.

11. onPC(self, pos)

현재 재생 위치를 전달하는 시그널을 방출합니다.

현재 재생 위치(pos)를 pc_signal 시그널로 방출하여 UI에 표시되도록 합니다.

 

이상으로 모든 설명을 마칩니다.

감사합니다.

댓글

이 블로그의 인기 게시물

Qt Designer 설치하기

PyQt5 기반 동영상 플레이어앱 만들기