PyQt5 기반 동영상 플레이어앱 만들기
비디오 재생
예전 게시물 중
음악파일 플레이어를 파이썬으로 만든 예제가 있습니다.
Qt의 QMediaPlayer, QMediaPlaylist class를 이용해 mp3 등
음악 파일을 재생하는 원리입니다.
아래와 같이 사용합니다.
from PyQt5.QtWidgets import QApplication from PyQt5.QtMultimedia import QMediaPlaylist, QMediaPlayer, QMediaContent from PyQt5.QtCore import QUrl import sys if __name__ == '__main__': app = QApplication(sys.argv) playlist = QMediaPlaylist() url = QUrl.fromLocalFile('test.mp3') playlist.addMedia(QMediaContent(url)) player = QMediaPlayer() player.setPlaylist(playlist) player.play() sys.exit(app.exec_())
PlayList를 생성해 파일을 추가하고, Player의 play() 함수를 수행하면 음악이
재생됩니다.
이번에는 동영상 플레이어 (Video Player) 를 Python + PyQt5를
이용해 만들어 보았습니다.
기본 원리는 위와 같지만 비디오을 출력할 위젯 (QVideoWidget)만 설정하면
동영상이 재생되는 원리입니다.
[동영상 재생 프로그램] |
주요기능
- 재생 리스트에 동영상 파일 추가, 삭제 (*.avi. *.mp4, *.mkv, *.mpg 등)
- 동영상 파일 재생, 일시정지, 앞, 뒤로, 볼륨제어
- 동영상 파일 재생 시간 표시 및 현재 위치 표시
- 재생위치 마우스 드래그를 통한 이동
개발 과정 and 소스 코드
전체 코드는 main.py 와 media.py 두개의 파이썬
파일로 구성 (main.py 파일 시작파일)
1. 기본 위젯 생성 및 컨트롤 배치 (main.py)
[Designer로 만든 UI] |
- 코드량을 줄이기 위해 Qt Designer 를 이용해 기본 UI 생성
- 재생, 정지 등을 위한 QPushButton 컨트롤
- 현재 재생 위치 표시, 볼륨제어 QSlider 컨트롤
- 재생 목록 표시 QListWidget 컨트롤
-
비디오 출력창 QVideoWidget 컨트롤 (QWidget에서 상속,
디자이너에서 처리)
- Qt Designer에서 만든 *.ui 파일 코드에서 불러오기
- 코드에서 컨트롤 시그널 연결 (예: 파일 재생 버튼 누름, 파일 추가 버튼 누름 등)
- 코드에서 컨트롤 시그널과 연결된 슬롯 함수 처리
main.py 소스코드
main.py 파일은 비디오 재생과 직접적인 관련이 없으며, 컨트롤이 배치된
UI (User Interface)를 생성해 앱을 화면에 출력하는 역할을 담당합니다
from PyQt5.QtWidgets import QApplication, QWidget, QFileDialog from PyQt5.QtCore import Qt, QUrl from PyQt5.QtGui import QPalette from PyQt5.uic import loadUi from media import CMultiMedia import sys import datetime QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) class CWidget(QWidget): def __init__(self): super().__init__() loadUi('main_ui.ui', self) # Multimedia Object self.mp = CMultiMedia(self, self.view) # video background color pal = QPalette() pal.setColor(QPalette.Background, Qt.black) self.view.setAutoFillBackground(True); self.view.setPalette(pal) # volume, slider self.vol.setRange(0,100) self.vol.setValue(50) # play time self.duration = '' # signal self.btn_add.clicked.connect(self.clickAdd) self.btn_del.clicked.connect(self.clickDel) self.btn_play.clicked.connect(self.clickPlay) self.btn_stop.clicked.connect(self.clickStop) self.btn_pause.clicked.connect(self.clickPause) self.btn_forward.clicked.connect(self.clickForward) self.btn_prev.clicked.connect(self.clickPrev) self.list.itemDoubleClicked.connect(self.dbClickList) self.vol.valueChanged.connect(self.volumeChanged) self.bar.sliderMoved.connect(self.barChanged) def clickAdd(self): files, ext = QFileDialog.getOpenFileNames(self , 'Select one or more files to open' , '' , 'Video (*.mp4 *.mpg *.mpeg *.avi *.wma)') if files: cnt = len(files) row = self.list.count() for i in range(row, row+cnt): self.list.addItem(files[i-row]) self.list.setCurrentRow(0) self.mp.addMedia(files) def clickDel(self): row = self.list.currentRow() self.list.takeItem(row) self.mp.delMedia(row) def clickPlay(self): index = self.list.currentRow() self.mp.playMedia(index) def clickStop(self): self.mp.stopMedia() def clickPause(self): self.mp.pauseMedia() def clickForward(self): cnt = self.list.count() curr = self.list.currentRow() if curr<cnt-1: self.list.setCurrentRow(curr+1) self.mp.forwardMedia() else: self.list.setCurrentRow(0) self.mp.forwardMedia(end=True) def clickPrev(self): cnt = self.list.count() curr = self.list.currentRow() if curr==0: self.list.setCurrentRow(cnt-1) self.mp.prevMedia(begin=True) else: self.list.setCurrentRow(curr-1) self.mp.prevMedia() def dbClickList(self, item): row = self.list.row(item) self.mp.playMedia(row) def volumeChanged(self, vol): self.mp.volumeMedia(vol) def barChanged(self, pos): print(pos) self.mp.posMoveMedia(pos) def updateState(self, msg): self.state.setText(msg) def updateBar(self, duration): self.bar.setRange(0,duration) self.bar.setSingleStep(int(duration/10)) self.bar.setPageStep(int(duration/10)) self.bar.setTickInterval(int(duration/10)) td = datetime.timedelta(milliseconds=duration) stime = str(td) idx = stime.rfind('.') self.duration = stime[:idx] def updatePos(self, pos): self.bar.setValue(pos) td = datetime.timedelta(milliseconds=pos) stime = str(td) idx = stime.rfind('.') stime = f'{stime[:idx]} / {self.duration}' self.playtime.setText(stime) if __name__ == '__main__': app = QApplication(sys.argv) w = CWidget() w.show() sys.exit(app.exec_())
-
__init__() 함수
- 메인 위젯 클래스 생성자 함수
- 부모 클래스 생성자 호출 및 Designer에서 만든 *.ui 불러오기 (라인 13~14)
- media.py 에 선언된 CMultiMedia 객체 생성 (라인 17)
- 비디오 출력 창, QVideoWidget 배경 검은색으로 변경 (라인 20~23)
- 볼륨 컨트롤, QSlider 범위 설정 (0~100) 및 초기치 설정 (라인 26~27)
- 버튼 클릭, 재생목록 더블클릭, 볼륨 변경 등 시그널, 슬롯 연결 (라인 33~43)
-
clickAdd() 함수
- 재생 목록 추가 버튼 클릭시 호출되는 슬롯 함수
- QFileDialog 클래스 재생 목록 파일 추가, 다수파일 선택 가능하며 결과는
리스트로 파일 경로 리턴 (라인 47~50)
[QFileDialog 생성] |
- 불러들인 파일 경로는 media.py 파일에 선언된 CMultiMedia (self.mp)
객체의 QMediaPlaylist로 전달해 재생 목록 작성 (라인 52~59)
clickDel() 함수
- 재생 목록 삭제 버튼 클릭시 호출되는 함수
- 재생 목록에 선택된 파일을 삭제, QMediaPlaylist 목록 또한 삭제 (라인
62~64)
clickPlay(), clickStop(), clickPause() 함수
- 재생, 정지, 일시정지 버튼 클릭시 호출되는 함수 (▶, ■, ❚❚)
- 각 재생 상태를 CMultiMedia class 객체에 선언된
QMediaPlayer로 전달 (라인 66~74)
clickForward(), clickPrev() 함수
- 앞, 뒤 버튼 클릭시 호출되는 함수 (▶▶, ◀◀)
- 앞으로 넘기기의 경우, 마지막 파일을 만나면 다시 처음으로 재생위치 변경
(라인 77~84)
- 뒤로 감기의 경우, 첫 파일을 만나면 마지막 파일로 재생위치 변경 (라인
87~94)
dbClickList() 함수
- 재생목록 리스트의 파일명을 더블클릭시 호출되는 함수
- 현재 목록의 파일 인덱스를 읽어와 바로 재생하도록 설정 (라인 97~98)
[파일리스트 더블클릭시 재생] |
volumeChanged() 함수
- 볼륨 컨트롤 (QSlider) 이 변경되면 호출되는 함수
- 슬라이더 컨트롤로 변경된 볼륨값 (0~100) 을 QMediaPlayer로 전달 (라인
101)
barChanged() 함수
- 마우스로 재생 상태 슬라이더 (QSlider)를 움직이면 호출되는
함수
- 재생 슬라이더는 동영상의 재생 시간을 범위로 가짐, 만약 30초
동영상이라면 0~30,000 ms (밀리초) 범위 설정
- 이동된 재생 시간으로 동영상 재생 위치 이동 (라인 105)
updateState() 함수
- 현재 파일의 재생상태 (Play, Stop, Pause)가 바뀔때 마다 호출되는
함수
- 동영상의 재생상태가 바뀜을 표시하기 위한 용도
- QMediaPlayer stateChanged 시그널 발생시 위젯으로 전달됨
(라인 108)
updatePos() 함수
- 동영상 파일이 재생될 때 마다 (기본 1초 간격) 호출되는 함수
- QMediaPlayer positionChanged 시그널 발생시 위젯으로 현재
재생 위치(ms) 전달됨
- 동영상 파일 재생시간에 따른 현재 위치를 슬라이더에 표시 (라인
121)
- 현재 재생시간을 문자 표시 예, (00:00:10 / 00:05:30), 5분 30초 영상의
10초 지점 (라인 122~126)
2. 동영상 재생 (media.py)
- QMediaPlayer, QMediaPlaylist 를 멤버 변수로 가지는 CMultiMedia class 생성
- 재생 상태, 재생 시간, 재생 위치를 파악할 시그널 생성
- 위젯으로 재생 정보를 전달할 사용자 정의 시그널 생성
media.py 소스코드
media.py 파일은 Qt의 Multimedia class를 이용해 동영상
재생 목록을 만들고, 재생하는 역할을 담당합니다.
from PyQt5.QtMultimedia import QMediaPlaylist, QMediaPlayer, QMediaContent from PyQt5.QtCore import QUrl, QObject, pyqtSignal class CMultiMedia(QObject): state_signal = pyqtSignal(str) duration_signal = pyqtSignal(int) position_signal = pyqtSignal(int) def __init__(self, widget, video_widget): super().__init__() self.parent = widget self.player = QMediaPlayer(widget, flags=QMediaPlayer.VideoSurface) self.player.setVideoOutput(video_widget) self.list = QMediaPlaylist() self.player.setPlaylist(self.list) # signal self.player.error.connect(self.errorHandle) self.player.stateChanged.connect(self.stateChanged) self.player.durationChanged.connect(self.durationChanged) self.player.positionChanged.connect(self.positionChanged) # user signal self.state_signal.connect(self.parent.updateState) self.duration_signal.connect(self.parent.updateBar) self.position_signal.connect(self.parent.updatePos) def addMedia(self, files): for f in files: url = QUrl.fromLocalFile(f) self.list.addMedia(QMediaContent(url)) def delMedia(self, index): self.list.removeMedia(index) def playMedia(self, index): self.list.setCurrentIndex(index) self.player.play() def stopMedia(self): self.player.stop() def pauseMedia(self): self.player.pause() def forwardMedia(self, end=False): if end: self.list.setCurrentIndex(0) else: self.list.next() def prevMedia(self, begin=False): if begin: cnt = self.list.mediaCount() self.list.setCurrentIndex(cnt-1) else: self.list.previous() def volumeMedia(self, vol): self.player.setVolume(vol) def posMoveMedia(self, pos): self.player.setPosition(pos) def stateChanged(self, state): msg = '' if state==QMediaPlayer.StoppedState: msg = 'Stopped' elif state==QMediaPlayer.PlayingState: msg = 'Playing' else: msg = 'Paused' self.state_signal.emit(msg) def durationChanged(self, duration): self.duration_signal.emit(duration) def positionChanged(self, pos): self.position_signal.emit(pos) def errorHandle(self, e): self.state_signal.emit(self.player.errorString())
__init__() 함수
- CMultiMedia 클래스 생성자 함수
- QMediaPlayer, QMediaPlaylist 멤버 변수로
생성
- 재생 정보를 얻기위해 QMediaPlayer Signal 생성
- 위젯으로 재생정보를 전달하기 위해 User Signal 생성
addMedia(), delMedia() 함수
- 재생 파일 목록을 전달 받아 QMediaPlaylist 목록
생성, 및 삭제 처리
-
play, stop, pause, forward, prev Media() 함수
- 위젯의 해당 버튼 클릭시 호출되는 함수
- QMediaPlayer와 연결된
QMediaPlaylist의 목록 재생, 정지, 일시정지 등
수행
volume, posMoveMedia() 함수
- 위젯의 볼륨, 재생위치 슬라이더 변경시 호출되는 함수
- QMediaPlayer 볼륨제어 및 재생 위치 변경
수행
stateChanged() 함수
- QMediaPlayer 상태 변경시 호출되는 함수
- Play, Stop, Pause 3가지 상태를 전달 받아 문자열로 변환
후 위젯에 전달
[출처 : Qt Asssist] |
durationChanged() 함수
- 재생 파일의 재생시간 정보가 변경시 호출되는 함수
- 밀리초(ms) 단위로 재생 시간 정보를 전달 받아 위젯으로
전달
- 파일 재생시간 초기화 용도
[출처 : Qt Asssist] |
positionChanged() 함수
- 동영상 파일이 재생되는 동안 매 1초 주기로 호출되는
함수
- 현재 재생위치를 밀리초 단위로 전달받아 위젯으로 전달
- 파일 진행 표시 용도
[출처 : Qt Asssist] |
이상으로 모든 설명을 마칩니다.
게시물에 유첨된 main.py, media.py 파일 2개를
이용, 프로젝트를 생성하고 main.py를 시작파일로
설정하면 비디오플레이어가 동작합니다.
그리고
main.ui 파일을 다운받아 해당 *.py 파일들과 같은 경로에 두어야
합니다.
해당 코드로 제작한 실행파일(*.exe)은 아래 링크를
클릭하면 다운로드 가능합니다.
- Pyinstaller 로 제작한 실행파일 : 비디오 플레이어
-
동영상이 재생되지 않는다면
Codec 을 설치해야 합니다.
- 개발 환경
Windows 10 pro, VS 2017, Python 3.7, PyQt5 5.14.2
참고 : PyQt6 용 동영상플레이어 게시물이 업데이트(2024.08)되었습니다.
안녕하세요~ 파이썬 고수이신것같아 질문드립니다ㅜㅜ
답글삭제파이썬으로 크롬열어서 url 입력해서 페이지 이동하려고 하는데
File "C:\ProgramData\Anaconda3\lib\site-packages\urllib3\util\retry.py", line 446, in increment
raise MaxRetryError(_pool, url, error or ResponseError(cause))
MaxRetryError: HTTPConnectionPool(host='127.0.0.1', port=11812): Max retries exceeded with url: /session/95931a094d2c73358b24bb37929adf22/title (Caused by NewConnectionError(': Failed to establish a new connection: [WinError 10061] 대상 컴퓨터에서 연결을 거부했으므로 연결하지 못했습니다'))
이런 에러가 자꾸 떠서 조치 방법 아시면 알려주시면 너무 감사하겠습니다(__)
게시물과 관련없는 내용에 대한 질문은 답변드리지 않으며, 저도 urllib3 이라는 site-package는 사용해 본 적이 없음을 양해 바랍니다.
삭제다음 urllib3 패키지 개발처 문서를 참조바랍니다.
https://urllib3.readthedocs.io/en/latest/user-guide.html
정성스러운 게시물 잘 보았습니다!
답글삭제많은 정보와 코드들로부터 많은 도움을 받아서 댓글 남깁니다! 감사드려요~~
네 감사합니다.
삭제혹시 현재 Qt Designer에서 메뉴표시줄(메뉴바)를 추가하고 싶은데 방법이 없을까요? 처음에 Widge으로 만들어져서 그런지 QMenuBar가 없네용..
삭제main.py 파일의 import 구문에서 QMainWindow Class를 불러온 후, CWidget을 QMainWindow에서 상속받으면 됩니다.
삭제답변 감사합니다 덕분에 해결했네요 ㅎㅎ
삭제예시 프로그램에서도 동영상 재생이 안되는데 코덱을 어떻게 깔아야 되나요? 스타코덱같은걸 깔아봤는데도 안되네요...
답글삭제K-Lite Codec을 한번 설치해보세요.
삭제대부분 오류는 코덱과 관련되어 있습니다.
예를 들면 아래와 같은 출력의 오류입니다.
DirectShowPlayerService::doRender: Unresolved error code 0x80040266 (IDispatch error #102)
윗 코드를 개조해서 파일 추가하면 바로 플레이되게 하고 싶은데 어떻게 수정하면 될까요?
답글삭제media.py에서
self.player.setMedia(QMediaContent(QUrl.fromLocalFile(files)))
이런 코드를 넣었는데 계속 파일 넣을때 튕기네요...
QMediaPlayerlist 의 객체인 self.list에 곡을 추가해야 합니다.
삭제해결 됐네요. 감사합니다!
삭제안녕하세요. 정성스런 게시물 감사드립니다. 한가지 질문이 있어 댓글 남깁니다.
답글삭제K-Lite 코덱 설치하여 파이참 환경에서는 동영상 재생이 정상적으로 되는것을 확인했습니다.
다만 별도 가상환경에서 pyinstaller 설치 후, exe로 변환하니 동영상 재생이 되질 않습니다.
UI의 나머지 widget들은 정상 작동하는걸로 보아 코덱 문제일 것 같은데, 코덱 설정을 가상환경에다가 추가로 반영해줘야 하는게 있을까요?
답변 남겨주시면 감사하겠습니다!
특정 동영상에서만 문제가 있다면 코덱 문제 (HEVC 등) 가능성이 높습니다.
삭제다만 pyinstaller 또한 python 환경에 따라 다양한 버그들이 존재하므로 배포 단계에서 문제 발생시 개발자 버전의 pyinstaller 설치를 시도해 보는 것도 하나의 방법입니다.
pip install https://github.com/pyinstaller/pyinstaller/tarball/develop
코덱 역시 다양한 (LAV Filter 등) 앱들이 있으므로 시도해 볼 필요가 있습니다.
이 강좌를 참고하여 비디오 플레이어 제작에 큰 도움이 되었습니다. 정말 감사합니다.
답글삭제한국어 강좌가 있는 것이 이렇게나 든든할 줄은 몰랐네요.
외람되오나 smi 자막 기능도 추가하고 싶은데, 혹시 참고할 만한 문서나 웹사이트가 있을련지요?
Qt 6에 subtitle이 추가되었습니다.
삭제New features in Qt 6 내용 중
Support for selection of audio, video and subtitle tracks when playing back media files has been added.
아래 링크 참조 바랍니다.
https://doc.qt.io/qt-6/qtmultimedia-changes-qt6.html
안녕하세요! 강좌 너무 잘봤고 감사드립니다.
답글삭제해당 코드 기반으로, 동시에 여러개의 동영상을 재생하도록 수정을하고자 합니다.
참고할만한 예제나, 변경 포인트가 있을까요!?
감사합니다~!
예제에 사용되는 QMediaPlayer Class의 객체를 여러개 생성해 리스트로 관리하면 됩니다.
삭제이 게시물의 내용이 이해된다면 그냥 여러개만 만들면 되는 간단한 개념입니다.
주말에도 답변 달아주셔서 감사드립니다!
삭제말씀하신대로 수정해서, 별도의 Playlist 및 재생까지 확인 했습니다~!
추가적으로, Control GUI와 VideoPlay GUI를 분리시키보려고 하는데요..
CMultiMedia class 안에 새로운 ui (VideoPlay GUI) 를 load 해서 사용하려고 하는데, 이런 컨셉이 맞는건지 잘 모르겠습니다.. ㅠ
class 상속에 대한 조언 부탁드립니다~!
구현이 된다면 Class 상속을 먼저 학습 후 시도해 보는 것이 좋겠습니다.
삭제참고로 위 구조는 상속의 "has A" 구조에 적절해 보입니다.
안녕하세요. 프로그래밍 초보자이며, 파이썬 공부중인데요.
답글삭제올려주신 코드를 그대로 실행을 했더니 아래와 같은 error가 발생하는데... 어떻게 해야하나요?
TypeError Traceback (most recent call last)
Cell In[1], line 131
129 if __name__ == '__main__':
130 app = QApplication(sys.argv)
--> 131 w = CWidget()
132 w.show()
133 sys.exit(app.exec_())
Cell In[1], line 17, in CWidget.__init__(self)
14 loadUi('main_ui.ui', self)
16 # Multimedia Object
---> 17 self.mp = CMultiMedia(self, self.view)
19 # video background color
20 pal = QPalette()
File D:\90_Python\python_note\Video_Player\media.py:17, in CMultiMedia.__init__(self, widget, video_widget)
15 self.player = QMediaPlayer(widget, flags=QMediaPlayer.VideoSurface)
16 #video_widget = QVideoWidget()
---> 17 self.player.setVideoOutput(video_widget)
18 self.list = QMediaPlaylist()
19 self.player.setPlaylist(self.list)
TypeError: arguments did not match any overloaded call:
setVideoOutput(self, a0: QVideoWidget): argument 1 has unexpected type 'QWidget'
setVideoOutput(self, a0: QGraphicsVideoItem): argument 1 has unexpected type 'QWidget'
setVideoOutput(self, surface: QAbstractVideoSurface): argument 1 has unexpected type 'QWidget'
setVideoOutput(self, surfaces: Iterable[QAbstractVideoSurface]): argument 1 has unexpected type 'QWidget'
게시물에 코드양을 줄이기 위해 Designer 를 이용해 *.ui 파일로 UI가 작성되어 있다고 언급되어 있습니다.
삭제그리고 *.ui 파일은 게시물의 마지막 부분에 링크가 걸려있습니다.
main.ui 파일 링크가 깨졌네요
답글삭제안녕하세요.
삭제확인해보니 링크는 깨지지 않았지만 알려주셔서 감사드립니다.
클릭 후 XML 형태로 열리지만, 구글 드라이브에서 다운로드 하면 됩니다.
참고로 *.UI 파일은 XML 형태로 Qt Control 의 정보를 표현합니다.
배속설정 기능 추가하고 싶은데 해당 기능과 관련하여 도움 좀 받을 수 있을까요??
답글삭제QMediaPlayer Class의 메서드 중,
삭제setPlaybackRate(qreal rate) 을 사용하고 rate 값을 입력하면 됩니다.
감사합니다 잘 해결되었습니다
삭제혹시 추가적으로 책갈피 기능처럼 특정 프레임 기억해두었다가 해당 프레임으로 돌아가도록 하는 기능 추가하기 위해서는 opencv를 따로 사용해주어야 할까요? 그리고 qt designer에서 해당 기능 사용 가능한 widget이 있다면 어떤 건지 알 수 있을까요??
특정 위치를 리스트 등으로 저장해두고 QMediaPlayer 메서드를 활용해 보세요.
삭제QMediaPlayer Class의 도움말을 한번 살펴보면 좋을 것 같습니다.