Qt 로 만들자: libvlc 를 이용한 간단한 동영상 재생기 LVPlayer
0. 들어가면서...
지난 번에 MPlayer 의 slave 모드를 이용해서 MPlayer 의 front-end 를 만드는 방법을 살펴보았다. 이번에는 오픈 소스 동영상 재생기 중에서 MPlayer 만큼이나 유명한 VLC Media Player 에 대해 알아보고자 한다.
VLC Media Player 는 MPlayer 와 달리 라이브러리 형태로 모든 기능을 제공한다. 이를 libvlc 또는 VLC SDK 라고 한다. 이론적으로 libvlc 를 이용하면 VLC Media Player 와 동등한 기능을 가진 동영상 재생기를 만들 수 있다.
이 프로젝트에서는 지난 MPGui 프로젝트에서처럼 아주 간단한 기능만 구현해보도록 하자. 그리고 MPGui 처럼 Windows 7 을 기본으로 하며, 사용된 VLC Media Player 버전은 2.2.4 이다. 그럼에도 불구하고, 다른 OS 나 플랫폼에서도 약간의 코드만 수정하면 충분히 작동할 것이다.
0.1. 사전 준비
libvlc 는 VLC Media Player 의 설치 프로그램에는 포함되어 있지 않다. 그렇다면 VLC Media Player 의 소스를 받아서 빌드해야 하느냐? 물론 그래도 되지만, 꼭 그럴 필요는 없다. 7-zip 배포본에는 sdk 디렉토리에 libvlc 가 포함되어 있다. 따라서 설치 프로그램이 아니라 7-zip 배포본을 받으면 된다.
0.1.1. libvlc( VLC SDK ) 설치하기
7-zip 배포본은 다음 사이트에서 받을 수 있다.
이 때 [Download VLC] 를 누르지 말고 옆에 있는 아래쪽 화살표(🔻)를 누르면 여러가지 형태의 배포본이 나온다. 그 중에서 [7-zip package] 를 선택하면 된다.
7-zip package 를 받았으면, 원하는 디렉토리에 풀면 된다.
0.1.2. PATH 환경 변수 바꾸기
VLC Media Player 는 SDK 를 DLL 형태로만 제공한다. 따라서 실행할 때에는 해당 DLL 들을 로드할 수 있어야 한다. 이를 위해 [PATH] 환경 변수를 바꿔야 한다. 바꾸는 방법은 다음과 같다.
이렇게 [PATH] 환경 변수에 VLC Media Player 의 경로를 추가하면 된다.
참고로, 내 경우에는 설치 프로그램으로 VLC Media Player v2.2.4 를 설치하였고, sdk 디렉토리만 7-zip package 에서 추출하였다.
1. 요구사항
2. 코드 분석
2.1 프로젝트 작성
2.1.1. 프로젝트 파일 수정(lvplayer.pro)
외부 라이브러리를 쓰기 위해 헤더 파일 경로와 라이브러리 경로를 설정해야한다. 컴파일러 환경 변수를 이용해도 되지만, 프로젝트 파일에서 추가하는 것이 더 확장성 있다. 추가된 내용은 다음과 같다.
SDKPATH 는 libvlc 가 설치되어 있는 디렉토리 경로이다. 위 경우는 7-zip package 의 sdk 디렉토리를 LVPlayer 프로젝트 하위 디렉토리에 복사한 것이다. 다른 경우라면 SDKPATH 를 환경에 맞게 수정해야 한다.
$$ 연산자는 qmake 에서 변수 값을 읽는데 쓰인다. PWD 는 프로젝트 파일이 있는 디렉토리 경로를 담고 있는 qmake 변수이다. INCLUDEPATH 는 헤더 파일의 경로를 나타내는 qmake 변수이고, += 연산자는 주어진 내용을 추가한다. LIBS 는 라이브러리를 찾을 디렉토리와 링크에 쓰이는 라이브러리를 나타내는 qmake 변수이다. libvlc 에 링크하기 위해서는 -lvlc 가 필요하다. Windows 환경에서도 qmake 가 알아서 -L 과 -l 옵션을 링커에 맞게 바꾸어준다.
2.2. 헤더 분석(lvplayer.h)
2.2.1. 헤더 파일 목록
2 번째 줄: libvlc 헤더는 vlc/vlc.h 이다.
2.2.2. public 멤버 함수
2.2.3. protected 멤버 함수
2.2.4. private 멤버 변수
2~7 번째 줄: libvlc_instance_t 는 libvlc 인스턴스를 나타내고, libvlc 를 쓰기 위해 가장 먼저 생성해야 한다. 그리고 여러 개의 인스턴스를 만드는 것도 가능하다. libvlc_media_player_t 는 미디어 플레이어를 나타내고, 이름에서 알 수 있듯이 재생 관련 기능을 담당한다. libvlc_event_manager_t 는 libvlc 에서 발생하는 이벤트들를 관리한다. libvlc 에서 이벤트 알림을 받고 싶을 때 쓰인다. 그리고 나중에 알아보겠지만, 미디어에 관련된 타입은 libvlc_media_t 이다.
2.2.5. private 멤버 함수
2.2.6. private slots
2.3. 소스 코드 분석(lvplayer.cpp)
2.3.1. LVPlayer 클래스
2.3.1.1. 생성자와 소멸자
8~16 번째 줄: libvlc_new() 는 새로운 libvlc 인스턴스를 생성한다. libvlc_new 는 main() 처럼 argc 와 argv 를 받는다. 특별히 넘겨줄 인수가 없으니 0, 0 을 넘겼다. 만약 libvlc 의 내부의 다양한 메세지를 보고 싶다면 다음처럼 하면 된다.
이뿐만 아니라, VLC Media Player 를 실행할 때 쓰이는 다양한 옵션들을 쓸 수 있다.
libvlc_media_player_new() 는 libvlc 인스턴스로부터 새로운 미디어 플레이어를 생성한다. libvlc_media_player_event_manger() 는 미디어 플레이어로부터 이벤트 관리자를 얻는다. libvlc_event_attach() 는 이렇게 얻은 이벤트 관리자의 특정한 이벤트에 콜백을 등록한다. 여기에서는 libvlc_MediaPlayerEndReached, 곧 끝까지 재생했을 때 발생하는 이벤트이다.
29~39 번째 줄: 리소스들을 정리한다. 재생을 중단하고, libvlc_event_detach() 로 콜백을 제거하고, libvlc_media_player_release() 로 미디어 플레이어를 해제하며, libvlc_release() 로 libvlc 인스턴스를 정리한다.
이쯤에서 눈썰미 좋은 사람들은 눈치챘을지도 모르지만, libvlc 의 일반적인 메모리 관리 패턴은 libvlc_object_new() 로 생성하고, libvlc_object_release() 로 해제한다.
이 프로젝트에서는 libvlc_new()/libvlc_release(), libvlc_media_player_new()/libvlc_media_player_release(), libvlc_media_new_path()/libvlc_media_release() 이다.
2.3.1.2. initMenus()
메뉴를 초기화한다.
2.3.1.3. initWidgets()
위젯을 초기화한다.
재생/정지 버튼을 만들고 가로로 배치한다.
동영상을 표시할 위젯을 생성하고 아랫쪽에 버튼을 배치한다.
8 번째 줄: QLayout::setContentsMargins() 는 레이아웃 바깔쪽 경계의 폭을 정한다. 인수는 왼쪽부터 시계 방향 순이다. 곧, 왼쪽, 윗쪽, 오른쪽, 아랫쪽 순이다.
2.3.1.4. fileOpen()
동영상 파일을 연다.
MPGui 때와 큰 차이가 없다.
2.3.1.5. setPlayText()
재생 버튼의 제목을 설정한다.
_playPush->setText() 를 여러 군데에서 직접 호출해서 재생 버튼의 제목을 바꿀 수도 있지만, 그렇게 하면 번역의 양이 늘어난다. 따라서 가능하면 이렇게 모음으로써 번역의 양을 줄일 수 있고, 아울러 번역 실수도 줄일 수 있다.
2.3.1.6. play()
동영상 파일을 재생한다.
7 번째 줄: 버튼을 클릭하면 입력 촛점이 버튼으로 옮겨진다. 이렇게 되면, 다음에 키보드를누르더라도 키보드 입력이 동영상 위젯에 전달되지 않는다. 따라서 입력 촛점을 동영상 위젯으로 옮겨야한다.
9~12 번째 줄: libvlc_media_player_is_playing() 는 현재 미디어가 재생 중인지 알려주고, libvlc_media_player_get_state() 는 재생중(libvlc_Playing), 일시정지(libvlc_Paused) 따위의 현재 플레이어 상태를 알려준다. libvlc_media_plyaer_is_playing() 은 libvlc_media_player_get_state() 의 값이 libvlc_Playing 또는 libvlc_Buffering 일 때와 같다.
15 번째 줄: QObject::sender() 는 슬롯을 호출한 시그널 객체를 뜻한다. 그런데 QObject::sender() 는 QObject::connect() 로 연결되지 않더라도 시그널 처리 중에 호출되더라도 값을 가질 수 있다. 예를 들어, 메뉴에서 열기를 했을 때에도 play() 가 호출되는데, 이 때에도 QObject::sender() 는 값이 가지며, 그 값은 QAction 객체를 가리킨다. 따라서 구체적으로 호출자를 확인하는 것이 좋다.
17 번째 줄: libvlc_media_player_set_pause() 는 일시정지/다시재생을 설정한다.
1~15 번째 줄: libvlc_media_t 는 미디어에 대한 타입이고, libvlc_media_new_path() 는 로컬 파일 시스템에 있는 미디어를 연다. qtv() 는 Qt 스타일 경로를 libvlc 스타일 경로로 바꾸어주는 유틸리티 함수이다. 나중에 다시 설명한다. 네트워크 스트림을 열고 싶다면, libvlc_media_new_location() 을 쓰면 된다. libvlc_media_player_set_media() 는 미디어 플레이어가 재생할 미디어를 설정한다. 일단 미디어 플레이어에 전달된 미디어는 libvlc_media_release() 로 해제해도 된다.
libvlc_media_player_set_hwnd() 는 동영상을 표시할 위젯을 설정한다. 다른 OS/플랫폼에서는 다른 함수를 써야 한다. 일반적인 형태는 다음과 같다.
2.3.1.7. stop()
재생을 중단한다.
10 번째 줄: libvlc_media_player_stop() 는 미디어 플레이어의 재생을 중단한다.
2.3.1.8. eventFilter()
동영상 위젯의 키보드 입력을 처리한다.
14 번째 줄: libvlc_media_player_get_time() 은 현재 재생 시간을 밀리초 단위로 알려준다.
20, 25 번째 줄: libvlc_media_player_set_time() 은 새로운 재생 위치를 밀리초 단위로 정한다.
2.3.2. 정적 함수
2.3.2.1. vlc_player_cb()
미디어 끝까지 재생했으면 재생을 중단한다.
6 번째 줄: struct libvlc_event_t 는 이벤트 종류이다. 콜백을 등록할 때 전달한 사용자 데이터가 두 번째 인수로 전달된다. libvlc_event_t.type 은 이벤트 종류를 나타낸다.
13 번째 줄: 재생을 중단하기 위해 LVPlayer::stop() 을 호출하는 코드이다. 콜백내에서 직접 호출하게 되면, DEAD-LOCK 상태에 빠지기 때문에, 이를 피하기 위해 Qt::QueuedConnection 을 이용하여 호출한다. 이렇게 하면 이벤트 큐에 추가하고나서 나중에 실행된다.
그런데 LVPlayer::stop() 은 private slots 이다. 그럼에도 불구하고, 외부 함수에서 호출이 가능한데, 이는 Qt 의 버그인 듯하다. 일단 문제가 발생하지 않기에 그냥 두기는 했지만, 원칙적으로는 LVPlayer::stop() 을 public slots 로 바꾸어야 한다.
2.3.2.2. qtv()
Qt 스타일 경로를 libvlc 스타일 경로로 바꾼다.
8 번째 줄: QDir::toNativeSeparators() 는 디렉토리 구분자를 OS 또는 플랫폼에 맞게 바꾸는 정적 멤버 함수이다. Windows 나 OS/2 의 경우 '\', 나머지는 '/' 이다.
libvlc 는 UTF-8 으로 인코딩된 파일 이름을 받는다. 특히 Windows 에서는 디렉토리 구분자로 '\' 만 받는다. 반면에 Qt 는 언제난 '/' 를 디렉토리 구분자로 쓴다. 그리고 내부적으로 UTF-16 을 쓰기 때문에 libvlc 에 파일 이름을 전달할 때는 위와 같은 변환이 필요한다.
QString::toUtf8() 은 QString 문자열을 UTF-8 QByteArray 문자열로 바꾼다. QByteArray::constData() 는 QByteArray 를 const char * 로 돌려준다.
3. 마무리하면서...
libvlc 를 이용해서 간단한 동영상 재생기를 만들어보았다. libvlc 는 이보다 훨씬 다양하고 강력한 기능들을 제공한다. 이러한 기능들에 대해서는 SDK 헤더를 살펴보기 바란다. 꼭 보기 바란다. 웬만한 기능들은 모두 구현되어 있으니 큰 어려움 없이 자신만의 동영상 재생기를 만들 수 있을 것이다.
다음은 Qt 에서 libvlc 를 이용해서 동영상 재생기를 만드는 방법을 설명하고 있는 사이트들이다. 꼭 읽어보자.
* 참고:
다음은 <LVPlayer> 의 실행 모습이다.
전체 소스는 여기에서 확인하자.
지난 번에 MPlayer 의 slave 모드를 이용해서 MPlayer 의 front-end 를 만드는 방법을 살펴보았다. 이번에는 오픈 소스 동영상 재생기 중에서 MPlayer 만큼이나 유명한 VLC Media Player 에 대해 알아보고자 한다.
VLC Media Player 는 MPlayer 와 달리 라이브러리 형태로 모든 기능을 제공한다. 이를 libvlc 또는 VLC SDK 라고 한다. 이론적으로 libvlc 를 이용하면 VLC Media Player 와 동등한 기능을 가진 동영상 재생기를 만들 수 있다.
이 프로젝트에서는 지난 MPGui 프로젝트에서처럼 아주 간단한 기능만 구현해보도록 하자. 그리고 MPGui 처럼 Windows 7 을 기본으로 하며, 사용된 VLC Media Player 버전은 2.2.4 이다. 그럼에도 불구하고, 다른 OS 나 플랫폼에서도 약간의 코드만 수정하면 충분히 작동할 것이다.
0.1. 사전 준비
libvlc 는 VLC Media Player 의 설치 프로그램에는 포함되어 있지 않다. 그렇다면 VLC Media Player 의 소스를 받아서 빌드해야 하느냐? 물론 그래도 되지만, 꼭 그럴 필요는 없다. 7-zip 배포본에는 sdk 디렉토리에 libvlc 가 포함되어 있다. 따라서 설치 프로그램이 아니라 7-zip 배포본을 받으면 된다.
0.1.1. libvlc( VLC SDK ) 설치하기
7-zip 배포본은 다음 사이트에서 받을 수 있다.
이 때 [Download VLC] 를 누르지 말고 옆에 있는 아래쪽 화살표(🔻)를 누르면 여러가지 형태의 배포본이 나온다. 그 중에서 [7-zip package] 를 선택하면 된다.
7-zip package 를 받았으면, 원하는 디렉토리에 풀면 된다.
0.1.2. PATH 환경 변수 바꾸기
VLC Media Player 는 SDK 를 DLL 형태로만 제공한다. 따라서 실행할 때에는 해당 DLL 들을 로드할 수 있어야 한다. 이를 위해 [PATH] 환경 변수를 바꿔야 한다. 바꾸는 방법은 다음과 같다.
- [시작] 메뉴 클릭
- [컴퓨터] 항목에서 오른쪽 버튼 클릭
- 팝업 메뉴에서 [속성] 클릭
- 왼쪽 패널에서 [고급 시스템 설정 클릭]
- [시스템 속성] 대화 상자에서 [환경 변수(N)...] 클릭
- [변수] 에서 [PATH] 환경 변수 선택
- [편집] 클릭 또는 [PATH] 환경 변수 더블 클릭
- [변수 값] 가장 마지막에 7-zip package 를 풀어 놓은 디렉토리(libvlc.dll 과 libvlccore.dll 이 있는 디렉토리) 추가(예: ....;vlc디렉토리)
이렇게 [PATH] 환경 변수에 VLC Media Player 의 경로를 추가하면 된다.
참고로, 내 경우에는 설치 프로그램으로 VLC Media Player v2.2.4 를 설치하였고, sdk 디렉토리만 7-zip package 에서 추출하였다.
1. 요구사항
- 동영상을 Qt 창에서 재생한다
- 탐색을 지원한다(10초 전후방 탐색)
- 재생/정지를 지원한다
- 일시정지를 지원한다
2. 코드 분석
2.1 프로젝트 작성
- 프로젝트 이름: lvplayer
- 메인 클래스 이름: LVPlayer
- 메인 클래스 유형: QMainWindow
2.1.1. 프로젝트 파일 수정(lvplayer.pro)
외부 라이브러리를 쓰기 위해 헤더 파일 경로와 라이브러리 경로를 설정해야한다. 컴파일러 환경 변수를 이용해도 되지만, 프로젝트 파일에서 추가하는 것이 더 확장성 있다. 추가된 내용은 다음과 같다.
SDKPATH = $$PWD/sdk
INCLUDEPATH += $$SDKPATH/includeLIBS += -L$$SDKPATH/lib -lvlc
SDKPATH 는 libvlc 가 설치되어 있는 디렉토리 경로이다. 위 경우는 7-zip package 의 sdk 디렉토리를 LVPlayer 프로젝트 하위 디렉토리에 복사한 것이다. 다른 경우라면 SDKPATH 를 환경에 맞게 수정해야 한다.
$$ 연산자는 qmake 에서 변수 값을 읽는데 쓰인다. PWD 는 프로젝트 파일이 있는 디렉토리 경로를 담고 있는 qmake 변수이다. INCLUDEPATH 는 헤더 파일의 경로를 나타내는 qmake 변수이고, += 연산자는 주어진 내용을 추가한다. LIBS 는 라이브러리를 찾을 디렉토리와 링크에 쓰이는 라이브러리를 나타내는 qmake 변수이다. libvlc 에 링크하기 위해서는 -lvlc 가 필요하다. Windows 환경에서도 qmake 가 알아서 -L 과 -l 옵션을 링커에 맞게 바꾸어준다.
2.2. 헤더 분석(lvplayer.h)
2.2.1. 헤더 파일 목록
1 2 3 4 5 | #include <QMainWindow> #include <vlc/vlc.h> #include <QtWidgets> |
2 번째 줄: libvlc 헤더는 vlc/vlc.h 이다.
2.2.2. public 멤버 함수
1 2 3 | public: LVPlayer(QWidget *parent = 0); ~LVPlayer(); |
2.2.3. protected 멤버 함수
1 2 | protected: bool eventFilter(QObject *o, QEvent *e); |
2.2.4. private 멤버 변수
1 2 3 4 5 6 7 8 9 10 11 12 13 | private: /// libvlc 인스턴스 libvlc_instance_t *_vlc; /// libvlc 미디어 플레이어 libvlc_media_player_t *_vlc_player; /// libvlc 이벤트 관리자 libvlc_event_manager_t *_vlc_player_event; QWidget *_movieWidget; ///< 동영상 위젯 QPushButton *_playPush; ///< 재생 버튼 QPushButton *_stopPush; ///< 정지 버튼 QString _movieFilePath; ///< 동영상 파일 경로 |
2~7 번째 줄: libvlc_instance_t 는 libvlc 인스턴스를 나타내고, libvlc 를 쓰기 위해 가장 먼저 생성해야 한다. 그리고 여러 개의 인스턴스를 만드는 것도 가능하다. libvlc_media_player_t 는 미디어 플레이어를 나타내고, 이름에서 알 수 있듯이 재생 관련 기능을 담당한다. libvlc_event_manager_t 는 libvlc 에서 발생하는 이벤트들를 관리한다. libvlc 에서 이벤트 알림을 받고 싶을 때 쓰인다. 그리고 나중에 알아보겠지만, 미디어에 관련된 타입은 libvlc_media_t 이다.
2.2.5. private 멤버 함수
1 2 3 4 | void initMenus(); void initWidgets(); void setPlayText(bool play = true); |
2.2.6. private slots
1 2 3 4 | private slots: void fileOpen(); void play(); void stop(); |
2.3. 소스 코드 분석(lvplayer.cpp)
2.3.1. LVPlayer 클래스
2.3.1.1. 생성자와 소멸자
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /** * @brief LVPlayer 생성자 * @param parent 부모 위젯 */ LVPlayer::LVPlayer(QWidget *parent) : QMainWindow(parent) { // libvlc 인스턴스 생성 _vlc = libvlc_new(0, 0); // libvlc 미디어 플레이어 생성 _vlc_player = libvlc_media_player_new(_vlc); // libvlc 이벤트 관리자 생성 _vlc_player_event = libvlc_media_player_event_manager(_vlc_player); // libvlc 이벤트 콜백 등록. 끝까지 재생했을 때 호출 libvlc_event_attach(_vlc_player_event, libvlc_MediaPlayerEndReached, vlc_player_cb, this); QApplication::setApplicationDisplayName(tr("LVPlayer")); initMenus(); initWidgets(); } /** * @brief LVPlayer 소멸자 */ LVPlayer::~LVPlayer() { // 정지 stop(); // 이벤트 콜백 제거 libvlc_event_detach(_vlc_player_event, libvlc_MediaPlayerEndReached, vlc_player_cb, this); // libvlc 미디어 플레이어 해제 libvlc_media_player_release(_vlc_player); // libvlc 인스턴스 해제 libvlc_release(_vlc); } |
8~16 번째 줄: libvlc_new() 는 새로운 libvlc 인스턴스를 생성한다. libvlc_new 는 main() 처럼 argc 와 argv 를 받는다. 특별히 넘겨줄 인수가 없으니 0, 0 을 넘겼다. 만약 libvlc 의 내부의 다양한 메세지를 보고 싶다면 다음처럼 하면 된다.
1 2 3 | const char *vlc_args[] = {"-vvv"}; _vlc = libvlc_new(sizeof(vlc_args) / sizeof(vlc_args[0]), vlc_args); |
이뿐만 아니라, VLC Media Player 를 실행할 때 쓰이는 다양한 옵션들을 쓸 수 있다.
libvlc_media_player_new() 는 libvlc 인스턴스로부터 새로운 미디어 플레이어를 생성한다. libvlc_media_player_event_manger() 는 미디어 플레이어로부터 이벤트 관리자를 얻는다. libvlc_event_attach() 는 이렇게 얻은 이벤트 관리자의 특정한 이벤트에 콜백을 등록한다. 여기에서는 libvlc_MediaPlayerEndReached, 곧 끝까지 재생했을 때 발생하는 이벤트이다.
29~39 번째 줄: 리소스들을 정리한다. 재생을 중단하고, libvlc_event_detach() 로 콜백을 제거하고, libvlc_media_player_release() 로 미디어 플레이어를 해제하며, libvlc_release() 로 libvlc 인스턴스를 정리한다.
이쯤에서 눈썰미 좋은 사람들은 눈치챘을지도 모르지만, libvlc 의 일반적인 메모리 관리 패턴은 libvlc_object_new() 로 생성하고, libvlc_object_release() 로 해제한다.
libvlc_object_new()
...
libvlc_object_release()
이 프로젝트에서는 libvlc_new()/libvlc_release(), libvlc_media_player_new()/libvlc_media_player_release(), libvlc_media_new_path()/libvlc_media_release() 이다.
2.3.1.2. initMenus()
메뉴를 초기화한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /** * @brief 메뉴를 초기화한다 */ void LVPlayer::initMenus() { QMenu *fileMenu = new QMenu(tr("파일(&F)")); fileMenu->addAction(tr("열기(&O)"), this, SLOT(fileOpen()), QKeySequence::Open); fileMenu->addSeparator(); fileMenu->addAction(tr("끝내기(&x)"), this, SLOT(close()), QKeySequence(tr("Ctrl+Q"))); menuBar()->addMenu(fileMenu); } |
2.3.1.3. initWidgets()
위젯을 초기화한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** * @brief 위젯을 초기화한다 */ void LVPlayer::initWidgets() { // 재생 버튼 _playPush = new QPushButton; setPlayText(); // 정지 버튼 _stopPush = new QPushButton(tr("정지(&S)")); // 가로로 배치 QHBoxLayout *hbox = new QHBoxLayout; hbox->addWidget(_playPush); hbox->addWidget(_stopPush); hbox->addStretch(1); |
재생/정지 버튼을 만들고 가로로 배치한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // 동영상 위젯 _movieWidget = new QWidget; _movieWidget->setAttribute(Qt::WA_OpaquePaintEvent); _movieWidget->installEventFilter(this); // 세로로 배치 QVBoxLayout *vbox = new QVBoxLayout; vbox->setContentsMargins(0, 0, 0, 0); vbox->addWidget(_movieWidget); vbox->addLayout(hbox); // 시그널/슬롯 연결 connect(_playPush, SIGNAL(clicked(bool)), this, SLOT(play())); connect(_stopPush, SIGNAL(clicked(bool)), this, SLOT(stop())); // 위젯 생성 QWidget *w = new QWidget; w->setLayout(vbox); setCentralWidget(w); // 초기 크기 resize(640, 480); // 입력 촛점 설정 _movieWidget->setFocus(); } |
동영상을 표시할 위젯을 생성하고 아랫쪽에 버튼을 배치한다.
8 번째 줄: QLayout::setContentsMargins() 는 레이아웃 바깔쪽 경계의 폭을 정한다. 인수는 왼쪽부터 시계 방향 순이다. 곧, 왼쪽, 윗쪽, 오른쪽, 아랫쪽 순이다.
2.3.1.4. fileOpen()
동영상 파일을 연다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /** * @brief 동영상 파일을 연다 */ void LVPlayer::fileOpen() { // 필터 목록 static QString filter(tr("비디오 (*.mkv *.mp4 *.avi *.mpg);;" "모든 파일 (*)")); // 파일 이름을 얻음 QString filePath(QFileDialog::getOpenFileName(this, QString(), QString(), filter)); // 파일을 선택했으면 재생 if (!filePath.isEmpty()) { _movieFilePath = filePath; play(); } } |
MPGui 때와 큰 차이가 없다.
2.3.1.5. setPlayText()
재생 버튼의 제목을 설정한다.
1 2 3 4 5 6 7 8 9 10 11 | /** * @brief 재생 버튼의 제목을 설정한다 * @param play 재생 버튼이면 true, 일시정지 버튼이면 false */ void LVPlayer::setPlayText(bool play) { if (play) _playPush->setText(tr("재생(&P)")); else _playPush->setText(tr("일시정지(&P)")); } |
_playPush->setText() 를 여러 군데에서 직접 호출해서 재생 버튼의 제목을 바꿀 수도 있지만, 그렇게 하면 번역의 양이 늘어난다. 따라서 가능하면 이렇게 모음으로써 번역의 양을 줄일 수 있고, 아울러 번역 실수도 줄일 수 있다.
2.3.1.6. play()
동영상 파일을 재생한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | /** * @brief 동영상을 재생한다 */ void LVPlayer::play() { // 입력 촛점은 언제나 동영상 위젯에 _movieWidget->setFocus(); // 재생 상태 int playing = libvlc_media_player_is_playing(_vlc_player); // 일시정지 상태 int paused = libvlc_media_player_get_state(_vlc_player) == libvlc_Paused; // 재생 버튼을 누른 경우에만 일시정지/재생 실행 if (sender() == _playPush && (playing || paused)) { libvlc_media_player_set_pause(_vlc_player, playing); setPlayText(playing); return; } // 동영상 파일을 아직 고르지 않았으면 파일을 열기부터 if (_movieFilePath.isEmpty()) { fileOpen(); return; } |
7 번째 줄: 버튼을 클릭하면 입력 촛점이 버튼으로 옮겨진다. 이렇게 되면, 다음에 키보드를누르더라도 키보드 입력이 동영상 위젯에 전달되지 않는다. 따라서 입력 촛점을 동영상 위젯으로 옮겨야한다.
9~12 번째 줄: libvlc_media_player_is_playing() 는 현재 미디어가 재생 중인지 알려주고, libvlc_media_player_get_state() 는 재생중(libvlc_Playing), 일시정지(libvlc_Paused) 따위의 현재 플레이어 상태를 알려준다. libvlc_media_plyaer_is_playing() 은 libvlc_media_player_get_state() 의 값이 libvlc_Playing 또는 libvlc_Buffering 일 때와 같다.
15 번째 줄: QObject::sender() 는 슬롯을 호출한 시그널 객체를 뜻한다. 그런데 QObject::sender() 는 QObject::connect() 로 연결되지 않더라도 시그널 처리 중에 호출되더라도 값을 가질 수 있다. 예를 들어, 메뉴에서 열기를 했을 때에도 play() 가 호출되는데, 이 때에도 QObject::sender() 는 값이 가지며, 그 값은 QAction 객체를 가리킨다. 따라서 구체적으로 호출자를 확인하는 것이 좋다.
17 번째 줄: libvlc_media_player_set_pause() 는 일시정지/다시재생을 설정한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | // 로컬 파일에 대한 미디어를 생성 libvlc_media_t *vlc_media = libvlc_media_new_path(_vlc, qtv(_movieFilePath)); // 미디어를 libvlc 미디어 플레이어에 등록 libvlc_media_player_set_media(_vlc_player, vlc_media); // 등록한 미디어는 해제 libvlc_media_release(vlc_media); // 동영상을 표시할 위젯 설정 libvlc_media_player_set_hwnd(_vlc_player, reinterpret_cast<void*>( _movieWidget->winId())); // 동영상 재생 libvlc_media_player_play(_vlc_player); // 재생 버튼 일시정지 상태로 setPlayText(false); } |
1~15 번째 줄: libvlc_media_t 는 미디어에 대한 타입이고, libvlc_media_new_path() 는 로컬 파일 시스템에 있는 미디어를 연다. qtv() 는 Qt 스타일 경로를 libvlc 스타일 경로로 바꾸어주는 유틸리티 함수이다. 나중에 다시 설명한다. 네트워크 스트림을 열고 싶다면, libvlc_media_new_location() 을 쓰면 된다. libvlc_media_player_set_media() 는 미디어 플레이어가 재생할 미디어를 설정한다. 일단 미디어 플레이어에 전달된 미디어는 libvlc_media_release() 로 해제해도 된다.
libvlc_media_player_set_hwnd() 는 동영상을 표시할 위젯을 설정한다. 다른 OS/플랫폼에서는 다른 함수를 써야 한다. 일반적인 형태는 다음과 같다.
1 2 3 4 5 6 7 | #if defined(Q_OS_MAC) libvlc_media_player_set_nsobject(_vlc, (void *)_movieWidget->winId()); #elif defined(Q_OS_UNIX) libvlc_media_player_set_xwindow(_vlc, _movieWidget->winId()); #elif defined(Q_OS_WIN) libvlc_media_player_set_hwnd(_vlc, (void *)_movieWidget->winId()); #endif |
2.3.1.7. stop()
재생을 중단한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /** * @brief 동영상 재생을 정지한다 */ void LVPlayer::stop() { // 입력 촛점은 언제나 동영상 위젯에 _movieWidget->setFocus(); // 재생을 정지 libvlc_media_player_stop(_vlc_player); // 재생 버튼 재생 상태로 setPlayText(true); } |
10 번째 줄: libvlc_media_player_stop() 는 미디어 플레이어의 재생을 중단한다.
2.3.1.8. eventFilter()
동영상 위젯의 키보드 입력을 처리한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | bool LVPlayer::eventFilter(QObject *o, QEvent *e) { if (o == _movieWidget) { QKeyEvent *ke = static_cast<QKeyEvent *>(e); // 재생 상태 int playing = libvlc_media_player_is_playing(_vlc_player); // 일시 정지 상태 int paused = libvlc_media_player_get_state(_vlc_player) == libvlc_Paused; // 현재 재생 시간 libvlc_time_t current = libvlc_media_player_get_time(_vlc_player); switch (ke->key()) { case Qt::Key_Left: // 10 초 이전으로 libvlc_media_player_set_time(_vlc_player, current - 10 * 1000); return true; case Qt::Key_Right: // 10 초 이후로 libvlc_media_player_set_time(_vlc_player, current + 10 * 1000); return true; case Qt::Key_Space: // 일시정지/재생 if (playing || paused) _playPush->click(); return true; } } // 나머지는 부모 클래스에게 return QMainWindow::eventFilter(o, e); } |
14 번째 줄: libvlc_media_player_get_time() 은 현재 재생 시간을 밀리초 단위로 알려준다.
20, 25 번째 줄: libvlc_media_player_set_time() 은 새로운 재생 위치를 밀리초 단위로 정한다.
2.3.2. 정적 함수
2.3.2.1. vlc_player_cb()
미디어 끝까지 재생했으면 재생을 중단한다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /** * @brief libvlc 미디어 플레이어 이벤트 콜백 * @param e libvlc 이벤트 종류 * @param p 사용자 데이터 */ static void vlc_player_cb(const struct libvlc_event_t *e, void *p) { // 끝까지 재생했으면 정지 if (e->type == libvlc_MediaPlayerEndReached) { LVPlayer *lvplayer = reinterpret_cast<LVPlayer *>(p); QMetaObject::invokeMethod(lvplayer, "stop", Qt::QueuedConnection); } } |
6 번째 줄: struct libvlc_event_t 는 이벤트 종류이다. 콜백을 등록할 때 전달한 사용자 데이터가 두 번째 인수로 전달된다. libvlc_event_t.type 은 이벤트 종류를 나타낸다.
13 번째 줄: 재생을 중단하기 위해 LVPlayer::stop() 을 호출하는 코드이다. 콜백내에서 직접 호출하게 되면, DEAD-LOCK 상태에 빠지기 때문에, 이를 피하기 위해 Qt::QueuedConnection 을 이용하여 호출한다. 이렇게 하면 이벤트 큐에 추가하고나서 나중에 실행된다.
그런데 LVPlayer::stop() 은 private slots 이다. 그럼에도 불구하고, 외부 함수에서 호출이 가능한데, 이는 Qt 의 버그인 듯하다. 일단 문제가 발생하지 않기에 그냥 두기는 했지만, 원칙적으로는 LVPlayer::stop() 을 public slots 로 바꾸어야 한다.
2.3.2.2. qtv()
Qt 스타일 경로를 libvlc 스타일 경로로 바꾼다.
1 2 3 4 5 6 7 8 9 | /** * @brief Qt 스타일 경로를 libvlc 스타일 경로로 바꾼다 * @param s Qt 경로 * @return libvlc 스타일 경로 */ static inline const char *qtv(const QString &s) { return QDir::toNativeSeparators(s).toUtf8().constData(); } |
8 번째 줄: QDir::toNativeSeparators() 는 디렉토리 구분자를 OS 또는 플랫폼에 맞게 바꾸는 정적 멤버 함수이다. Windows 나 OS/2 의 경우 '\', 나머지는 '/' 이다.
libvlc 는 UTF-8 으로 인코딩된 파일 이름을 받는다. 특히 Windows 에서는 디렉토리 구분자로 '\' 만 받는다. 반면에 Qt 는 언제난 '/' 를 디렉토리 구분자로 쓴다. 그리고 내부적으로 UTF-16 을 쓰기 때문에 libvlc 에 파일 이름을 전달할 때는 위와 같은 변환이 필요한다.
QString::toUtf8() 은 QString 문자열을 UTF-8 QByteArray 문자열로 바꾼다. QByteArray::constData() 는 QByteArray 를 const char * 로 돌려준다.
3. 마무리하면서...
libvlc 를 이용해서 간단한 동영상 재생기를 만들어보았다. libvlc 는 이보다 훨씬 다양하고 강력한 기능들을 제공한다. 이러한 기능들에 대해서는 SDK 헤더를 살펴보기 바란다. 꼭 보기 바란다. 웬만한 기능들은 모두 구현되어 있으니 큰 어려움 없이 자신만의 동영상 재생기를 만들 수 있을 것이다.
다음은 Qt 에서 libvlc 를 이용해서 동영상 재생기를 만드는 방법을 설명하고 있는 사이트들이다. 꼭 읽어보자.
* 참고:
다음은 <LVPlayer> 의 실행 모습이다.
전체 소스는 여기에서 확인하자.
개발자님의 코드를 제 컴에서 실행하려고 합니다.
답글삭제깃에 올라와있는 lvplayer.pro 코드 중,
lvplayer.pro 코드에서 20번째 line부터
SDKPATH = $$PWD/sdk
INCLUDEPATH += $$SDKPATH/include
LIBS += -L$$SDKPATH/lib -lvlc
해당 부분이 vlc lib 7-zip package 를 압축해제한 후, 해당 절대경로를 입력해주었습니다.
SDKPATH = C:\Users\stuck\Desktop\vlc-3.0.16\sdk
INCLUDEPATH += C:\Users\stuck\Desktop\vlc-3.0.16\sdk\include
LIBS += -LC:\Users\stuck\Desktop\vlc-3.0.16\sdk\lib
이렇게 해주었는데, 에러코드가 71개가 뜹니다..
에러나는 부분은 lvplayer.cpp, lvplayer.h에서 나고있는데, 사진 첨부가 안되어 모든 에러를 설명드릴수는 없지만
'libvlc_instance_t' does not name a type
'libvlc_media_player_t' does no name a type
'QpushButton' does not name a type 등등이 있고
use of undeclared identifier 'lvPlayer' 등이 있는데
뭔가 제가 정의도 안해주고 설정도 안해준 것 같습니다.
제 컴에서 개발자님 코드 실행을 시키고 싶은데 뭐가 부족한건가요? 도움 부탁드립니다...
SDKPATH 값만 바꾸면 INCLUDEPATH 나 LIBS 는 자동으로 바뀌도록 되어 있으니 SDKPATH 값만 설정하시면 됩니다. 그리고 디렉토리 구분자를 역슬래시(\)가 아니라 슬래시(/)를 사용하셔야 합니다. 그래도 안 되시면 다시 댓글 달아주세요.
삭제C:\Users\stuck\Desktop\lvplayer\lvplayer.h:32: error: 'QPushButton' does not name a type
삭제..\lvplayer\lvplayer.h:32:5: error: 'QPushButton' does not name a type
QPushButton *_playPush; ///< ъ깮 踰꾪듉
^~~~~~~~~~~
C:\Users\stuck\Desktop\lvplayer\lvplayer.h:25: error: 'libvlc_instance_t' does not name a type
In file included from ..\lvplayer\main.cpp:1:
..\lvplayer\lvplayer.h:25:5: error: 'libvlc_instance_t' does not name a type
libvlc_instance_t *_vlc;
^~~~~~~~~~~~~~~~~
vlc 라이브러리를 못먹어서 에러가 나는것같습니다...
그리고 qpushbutton은 왜 에러가 나는걸까요?
lvplayer.cpp
lvplayer.h
lvplayer.pro
main.cpp
만 있으면 안되는건가요?
SDKPATH = $$PWD/C:/Users/stuck/Desktop/vlc-3.0.16/sdk
INCLUDEPATH += $$SDKPATH/include
LIBS += -L$$SDKPATH/lib
이렇게 바꿔보았습니다.
1. 다른 것은 건들지 마시고, SDKPATH 만 이렇게 설정하세요.
삭제SDKPATH = C:/Users/stuck/Desktop/vlc-3.0.16/sdk
2. QPushButton 에러는 Qt 헤더가 적절히 포함되지 않아서 생기는 문제로 보입니다. 다른 프로젝트는 문제가 없다면 위 설정 때문에 발생하는 문제일 수도 있으니 일단 위 설정을 바꿔 보시고 다시 시도해 보세요.
3. 필요한 파일은 말씀하신 4개 파일만 있으면 됩니다. 프로젝트 별로 디렉토리를 구성하였으니 디렉토리에 있는 파일을 모두 받으면 됩니다.
성공을 기원합니다~^^
1. mingw로 하셨나요? 아니면 msvc 2015 나 2017 32bit 등 어떤것으로 하셨는지 궁금합니다.
삭제2. ui 파일 없이 어떻게 가능한건지 궁금합니다.
아직 성공을 못했습니다..... 도와주시면 감사하겠습니다...
아 그리고
삭제LNK1107: 파일이 잘못되었거나 손상되었습니다. 0x12에서 읽을 수 없습니다.
C:
C:\work\QT_project\lvplayer\vlc\sdk\lib\vlc.lib
라는 에러와
창에
vlc.lib
INPUT(libvlc.lib)
이란 코드가 떴습니다.
혹시, vlclib를 (sdk 폴더 있는) 7zip으로 받고 프로젝트 폴더에 그대로 넣으면 되는거 아닌가요? vlc 라이브러리에는 따로 해줄건 없는거 아닌가요? 왜 vlc.lib를 못읽는건가요.....
아! 저 위에 2개는 해결이 되었습니다.
삭제이제 실행이 됩니다! 그런데 파일에서 동영상 가져와서 play가 안됩니다..
main input error:입력을 열 수 없습니다
main input error: VLC에서 'file:///C:/내path/build-lvplayer-Desktop_Qt_5_12_11_MSVC2017_32bit-Debug/%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD' MRL을 열 수 없습니다. 자세한 내용은 로그를 확인하세요..
이건 왜그런걸까요.....
오~ 많이 진행됐네요~ 해당 메시지는 인코딩 관련 오류인 것 같은데, 혹시 이름이 영어나 숫자로만 되어 있는 파일을 열어도 같은 문제가 발생하는지 확인해 보세요.
삭제그리고
1. 저는 qt5 에 포함되어 있는 mingw 를 썼고,
2. ui 파일 대신에 위젯과 레이아웃 함수를 써서 직접 화면을 구성했습니다. LVPlayer::initMenus() 와 LVPlayer::initWidgets() 함수가 그 일을 합니다.
하.. 이게 뭐하는건지 모르겠네요 하하하...
삭제저 위에거는 해결이 또 됐습니다...
QFileDialog 로 동영상 가져오는 것이 안됩니다.
main stream debug: (path: C:\work\QT_project\build-lvplayer-Desktop_Qt_5_12_11_MSVC2017_32bit-Debug\????????????????????????????????????????????????????)
main stream debug: looking for access module matching "file": 27 candidates
filesystem stream error: cannot open file C:\work\QT_project\build-lvplayer-Desktop_Qt_5_12_11_MSVC2017_32bit-Debug\???????????????????????????????????????????????????? (No such file or directory)
main stream debug: no access modules matched
main input error: 입력을 열 수 없습니다
main input error: VLC에서 'file:///C:/work/QT_project/build-lvplayer-Desktop_Qt_5_12_11_MSVC2017_32bit-Debug/%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD' MRL을 열 수 없습니다. 자세한 내용은 로그를 확인하세요.
main audio output debug: removing module "directsound"
main libvlc debug: exiting
main libvlc debug: no exit handler
main libvlc debug: removing all interfaces
main keystore debug: removing module "memory"
[00d30cc0] mmdevice audio output error: cannot initialize COM (error 0x80010106)
[02f3de08] filesystem stream error: cannot open file C:\work\QT_project\build-lvplayer-Desktop_Qt_5_12_11_MSVC2017_32bit-Debug\硼硼硼硼硼硼硼硼硼硼硼硼硼硼硼硼硼硼硼硼硼硼硼硼硼硼 (No such file or directory)
[02f5ddb8] main input error: 낅젰놁뒿덈떎
[02f5ddb8] main input error: VLC먯꽌 'file:///C:/work/QT_project/build-lvplayer-Desktop_Qt_5_12_11_MSVC2017_32bit-Debug/%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD%DD' MRL놁뒿덈떎. 먯꽭댁슜€ 濡쒓렇瑜뺤씤섏꽭
18:10:09: C:\work\QT_project\build-lvplayer-Desktop_Qt_5_12_11_MSVC2017_32bit-Debug\debug\lvplayer.exe exited with code 0
왜 파일을 읽어올때
'file:///C: ~~~
///이게 왜 3개인지, 이 부분을 수정할 수 있는 코드는 어디인지 여쭤보고 싶습니다. 위에질문들은 무시하셔도 됩니다!
어 댓글 남겨주신걸 못봤네요.. 답변 정말로 감사드립니다. 영어로만 되어있는 avi나 mkv mp4 모두 에러가 납니다. 왜 파일 불러오는게 안될까요...
삭제일단, file:/// 은 정상입니다. UNIX 스타일 표시법을 Windows 에도 적용한 결과입니다.
삭제원본 파일이름이 무엇인가요? 파일 이름 부분만 저렇게 변환되는 게 이상하네요.
파일 이름은 abc.mkv, 123.mp4, yolo.avi, 2-2_21091023.avi 등 다 해보았는데 똑같습니다..
삭제위에 댓글과 똑같은 에러 문구가 나오면서 mrl을 열수없습니다.
라고 뜹니다.
https://forum.videolan.org/viewtopic.php?t=104939
https://github.com/caprica/vlcj-javafx-demo/issues/38
https://stackoverflow.com/questions/15221855/vlc-media-open-failure
https://forum.videolan.org/viewtopic.php?t=146741
등이 구글링해서 비슷한 것 찾은건데.. 해결이 안됩니다.. 왜 파일이름을 못알아듣고 mrl이 뭔지... 도와주실 수 있나요....
아아아 됐습니다!!!! mingw로 하니까 됩니다...흐그흫휴ㅠ규ㅠㅜㅜㅜ
삭제이 코드가 msvc에서는 안되나봐요.... 코드 감사드립니다.
혹시 여기 코드에서 더 나아가서 진짜 vlc처럼 재생목록, 메타데이터, 아래 바, 시간 등등 나오게 하려면 어떻게 더 구글링하면 좋을지 알려주시면 감사하겠습니다!!
제 질문들 답변해주셔서 정말 감사드립니다!
아 그리고 혹시 LV플레이어에서 LV뜻이 뭔가요?
삭제계속 궁금했습니다...
오~ 축하드려요~^^ 컴파일러 문제일 줄이야, 생각도 못했네요.
삭제VLC 기능에 대해서는 VLC 사이트에 있는 문서들을 꼭 읽어보세요. 그리고 뭐니 뭐니 해도 오픈 소스이니 소스를 들여다 보는 게 가장 좋겠죠. 그럼에도 가장 먼저 할 만한 것은 vlc sdk 의 헤더 파일을 보시고 어떤 기능이 있는지 보시는 것입니다.
MRL 은 Media Resource Locator의 약자입니다. 자세한 것은 https://wiki.videolan.org/Media_resource_locator/ 를 보세요.
LV 는 별 뜻 없답니다. libvlc 약자예요. ^^
계속 개량하셔서 멋진 동영상 재생기가 나오기를 기대하겠습니다~
와우 자세한 설명까지.. 감사합니다 엄청 정성이 담긴글이네요
답글삭제고맙습니다!
삭제안녕하세요 문의 사항이 있어 글을 남깁니다.
답글삭제임베디드 리눅스 환경에서 vlc를 사용하려고 하는데요
혹시 x11이나 wayland 환경이 아닌 상태에서도 vlc를 사용할 수 있나요?
libvlc_media_player_set_xwindow(_vlc, _movieWidget->winId());
위의 함수를 보니 xwindow 환경에서만 사용 가능한것 같아서요
두서 없는 질문 죄송합니다.
그리고 감사합니다.