Qt 로 만들자: 간단한 MPlayer front-end MPGui

0. 들어가면서...

미루고 미루다가 이제서야 글을 쓴다. Qt 에 쓸 시간이 왜 이리도 안나는지.

이번에는 MPlayer front-end 이다. MPlayer 는 오픈소스 동영상 재생기 중에서 가장 유명한 재생기 중의 하나이다. 하지만, 명령행에서 실행되기 때문에 익숙하지 않으면 꽤 불편하다. 물론 GTK GUI 가 제공되기는 하지만, 많이 쓰이지는 않는듯 하다. 이보다는 오히려 SMPlayer 라는 front-end 가 더 유명하다.

SMPlayer 라는 front-end 가 생길 수 있었던 이유는 MPlayer 에서 slave 모드라는 독특한 기능을 제공하기 때문이다. slave 모드는 MPlayer 가 독립적으로 실행되는 것이 아니라 다른 프로그램의 back-end 로 실행되는 것이다. 따라서 MPlayer 의 front-end 에서 MPlayer 의 다양한 기능을 조절할 수 있다.

MPlayer 의 slave 모드를 이용해서 어떻게 MPlayer front-end 를 만들 수 있는지 간단히 알아보도록 하자. 이 과정에서 프로그램의 실행과 파이프 처리에 대해서 알 수 있을 것이다.

1. 요구사항

  • 동영상을 Qt 창에서 재생한다
  • 탐색을 지원한다
  • 재생/정지를 지원한다
  • 일시정지를 지원한다

위 기능들을 구현할 것이다. 다만, Windows 환경만을 고려한다. 다른 OS/Platform 에서도 작동할 수 있지만, OS/Platform 에 따라 추가 코드가 필요할 수 있다. Windows 용 MPlayer 실행파일은 다음 사이트에서 구한 것을 사용했다.


2. 코드 분석

2.1. 프로젝트 작성

  • 프로젝트 이름: mpgui
  • 메인 클래스 이름: MPGui
  • 메인 클래스 유형: QMainWindow

2.2. 헤더 분석(mpgui.h)

2.2.1 헤더 파일 목록

1
2
3
#include <QMainWindow>

#include <QProcess>


3 번째 줄: QProcess 는 프로세스 관리를 위해 필요한 헤더이다.

2.2.2 public 함수

1
2
3
public:
    MPGui(QWidget *parent = 0);
    ~MPGui();


2.2.3 protected 함수

1
2
protected:
    bool eventFilter(QObject *o, QEvent *e) Q_DECL_OVERRIDE;


2 번째 줄: QWidget::eventFilter() 는 대상 객체의 이벤트를 필터할 때 쓰인다. Q_DECL_OVERRIDE 는 해당 멤버 함수가 재정의되고 있음을 명시적으로 나타내는 것이다. 부모 클래스에 해당 멤버 함수가 없을 때는 오류가 발생한다. 재정의할 때 쉽게 저지를 수 있는 실수를 방지해준다. C++11 이상에서만 효과가 있다.

2.2.4 private 멤버 변수

1
2
3
4
private:
    QProcess _mplayer;      ///< MPlayer 프로세스
    QString _mplayerPath;   ///< MPlayer 경로
    QString _movieFilePath; ///< 동영상 파일 경로


2 번째 줄: QProcess 클래스는 프로세스를 관리하는 클래스이며, QIODevice 클래스를 상속했기 때문에 읽기 쓰기가 가능하다.이 기능을 이용하면 자식 프로세스의 입출력을 리디렉션할 수 있다. 바로 파이프 기능이다.

2.2.5 private 멤버 함수

1
2
3
4
    void initMenus();
    void initWidgets();

    void play(const QString &fileName);


2.2.6 private slots

1
2
private slots:
    void fileOpen();


2.3. 소스 코드 분석(mpgui.cpp)

2.3.1 MPGui 클래스

MPlayer 의 front-end 클래스이다.

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
/**
 * @brief MPGui 생성자
 * @param parent 부모 위젯
 */

MPGui::MPGui(QWidget *parent)
    : QMainWindow(parent)
    , _mplayerPath("mplayer.exe")
{
    // 애플리케이션 표시 이름 설정
    QApplication::setApplicationDisplayName(tr("MPGui"));

    initMenus();
    initWidgets();
}

/**
 * @brief MPGui 소멸자
 */

MPGui::~MPGui()
{
    // MPlayer 가 끝날 때까지 기다림
    _mplayer.write("quit\n");
    _mplayer.waitForFinished();
}


10 번째 줄: QApplication::setApplicationDisplayName() 은 프로그램의 표시 이름을 설정하는 정적 멤버함수이다. 이는 메인창 제목의 기본값으로도 쓰인다.

22~23 번째 줄: 부모 프로그램이 종료되면 자식 프로그램도 같이 종료되지만, 자식 프로그램이 리소스등을 정리하면서 종료할 수 있도록 한다. QProcess::write() 는 주어진 문자열을 자식 프로세스의 표준 입력으로 전달한다. 오버로딩된 QProcess::write() 도 있으니, Assistant 를 살펴보기 바란다. QProcess::waitForFinished() 는 자식 프로세스가 끝날 때까지 기다린다. 주의할 것은 GUI 쓰레드에서 호출하면 쓰레드 자체가 멈추기 때문에 사용자 입장에서는 프로그램이 먹통이 되었다고 생각할 수 있다. 따라서 QProcess::waitForFinished() 를 GUI 쓰레드에서 쓸 때에는 주의해야 한다.

2.3.1.2. initMenus()

프로그램에서 사용할 메뉴들을 생성한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
 * @brief 메뉴를 초기화한다
 */

void MPGui::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
18
19
20
/**
 * @brief 위젯을 초기화한다
 */

void MPGui::initWidgets()
{
    QWidget *w = new QWidget;
    // 위젯 속성 설정: 위젯의 모든 영역을 직접 그림
    w->setAttribute(Qt::WA_OpaquePaintEvent);
    // 이벤트 필터 설치
    w->installEventFilter(this);

    // 중앙 위젯으로 설정
    setCentralWidget(w);

    // 기본 크기 설정
    resize(640, 480);

    // 입력 촛점 설정
    w->setFocus();
}


8 번째 줄: QWidget::setAttribute() 는 위젯의 속성을 설정한다. Qt::WA_OpaquePaintEvent 는 위젯의 모든 영역을 위젯에서 제어하고자 할 때 쓰인다. 우리는 위젯에 동영상을 재생할 것이기 때문에 이 속성이 적절하다. 없더라도 큰 문제는 없지만, 약간의 성능 향상이 있다고 문서에 쓰여있다.

10 번째 줄: QWidget::installEventFilter() 는 해당 위젯의 이벤트를 필터/모니터할 객체를 지정한다. 위의 경우 위젯 w 에서 발생하는 이벤트는 모두 this(MPGui) 객체의 eventFilter() 멤버 함수에 전달된다. 이 함수의 결과에 따라 위젯 w 에 이벤트가 전달되기도 하고, 차단되기도 한다. 이 기능은 위젯의 소스를 수정할 수 없거나, 해당 위젯 클래스를 상속해서 이벤트 처리를 하기 귀찮을 때 무척 유용하다.

2.3.1.4. fileOpen()

사용자가 [열기] 메뉴를 선택했을 때 호출된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
 * @brief 동영상 파일을 열고 재생한다
 */

void MPGui::fileOpen()
{
    // 필터 목록
    static QString filter(tr("비디오 (*.mkv *.mp4 *.avi *.mpg);;"
                             "모든 파일 (*)"));
    // 파일 이름을 얻음
    QString filePath(QFileDialog::getOpenFileName(this, QString(), QString(),
                                                  filter));

    // 파일을 선택했으면 재생
    if (!filePath.isEmpty())
        play(filePath);
}


7~11 번째 줄: QFileDialog::getOpenFileName() 은 [파일 열기] 대화상자를 제공하는 정적 멤버 함수이다. 2 번째와 3번째 매개 변수는 각각 캡션과 처음에 보여줄 디렉토리이다. 4 번째는 필터 목록이다. 이 필터는 "필터이름 (필터마스크1 필터마스크2 ...)" 형태로 주어진다. 여러개의 필터를 제공해야 하는 경우, 7~8 번째 줄에 보이듯이 필터와 필터 사이를 ";;" 로 연결하면 된다. 참고로, 필터 마스크가 '*' 이면 필터 목록에 필터 이름만 보이고 필터 마스크는 보이지 않는다. 보다 자세한 것은 Assistant 를 보도록 하자.

2.3.1.5. 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
30
31
32
33
34
35
36
37
/**
 * @brief MPlayer 로 재생
 * @param filePath 동영상 파일 경로
 */

void MPGui::play(const QString &filePath)
{
    QStringList mplayerOptions;

    // MPlayer 가 이미 실행중이면, 기존 MPlayer 종료
    if (_mplayer.state() != QProcess::NotRunning)
    {
        _mplayer.write(QString("loadfile %1\n").arg(filePath).toLocal8Bit());
        return;
    }

    // MPlayer 옵션 설정
    // -slave: 슬레이브 모드
    // -wid: 동영상을 보여줄 창 ID
    // -colorkey: 오버레이에 쓰일 색상. 기본은 녹색
    // -quiet: 조용히
    mplayerOptions << "-slave"
                   << "-wid" << QString::number(centralWidget()->winId())
                   << "-colorkey" << "0x010101"
                   << "-quiet"
                   << filePath;

    // MPlayer 실행
    _mplayer.start(_mplayerPath, mplayerOptions, QIODevice::WriteOnly);

    // MPlayer 를 실행하지 못했으면, 오류 메세지 보여줌
    if (_mplayer.state() == QProcess::NotRunning)
        QMessageBox::warning(this, QApplication::applicationDisplayName(),
                             tr("MPlayer 를 실행할 수 없습니다."));

    // 동영상 파일 경로 저장. 이벤트 필터에서 쓰임
    _movieFilePath = filePath;
}


9~14 번째 줄: QProcess::state() 는 프로세스의 실행 상태를 나타낸다. QProcess::NotRunning 은 실행되고 있지 않다는 뜻이다. QProcess::Starting 은 시작 준비 중이라는 뜻이고, QProcess::Running 은 프로그램이 실행되고 있다는 뜻이다.
MPlayer 가 이미 실행중이라면 동영상 파일을 바로 로드해서 재생한다. QString::toLocal8Bit() 은 QString 문자열을 시스템 문자셋 문자열로 바꾼다.

16~25 번째 줄: MPlayer 옵션들이다. [-slave] 는 MPlayer 를 slave 모드로 시작하도록 한다. [-wid] 는 동영상을 재생할 창의 ID 이다. 창의 ID 를 얻으려면 QWidget::winId() 를 이용하면 된다. QWidget::winId() 는 위젯의 네이티브 창 ID 를 돌려준다. [-colorkey] 는 동영상을 표시할 영역을 칠할 색상이다. MPlayer 의 기본값은 0x00ff00 이므로 녹색이다. 만약 창의 빈 영역이 있다면 이 색상으로 칠해진다. [-quiet] 는 불필요한 메세지를 나타내지 않도록 한다. 마지막에 파일 경로를 추가했는데, 파일 경로가 없으면 MPlayer 는 실행을 바로 종료한다. 나중에 "loadfile" 명령을 통해 동영상을 재생하고 싶다면 [-idle] 옵션을 추가해야 한다.

28 번째 줄: QProcess::start() 는 프로그램을 실행한다. PATH 환경 변수에서 프로그램을 찾아 실행한다. 세 번째 매개변수는 접근 모드를 지정하는데, QIODevice::WriteOnly 는 쓰기 전용이다. 다시 말해서, MPlayer 의 표준 입력으로만 전달한다는 뜻이다. MPlayer 의 표준 출력을 읽고 싶다면 QIODevice::ReadOnly 를 쓰면 되고, 읽기/쓰기를 모두하고 싶다면 QIODevice::ReadWrite 를 쓰면 된다. 필요없다면 QIODevice::NotOpen 을 쓰자. 이외에도 몇 가지 플래그들이 더 있으니 Assistant 를 확인하자.

30~33 번째 줄: QProcess::start() 는 바로 QProcess::Starting 상태로 들어간다. 따라서 실행 직후 상태가 QProcess::NotRunning() 상태라면 MPlayer 를 실행하지 못했다는 뜻이다. 물론 매우 빨리 종료했을 수도 있지만, 이 경우는 어쨌든 오류 상태이므로 사용자에게 알린다.

2.3.1.6. 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
/**
 * @brief 이벤트를 필터한다
 * @param o 필터할 객체
 * @param e 발생한 이벤트
 * @return 필터링했으면 true, 그렇지 않으면 false
 */

bool MPGui::eventFilter(QObject *o, QEvent *e)
{
    // MPlayer 실행 중일 때에만 필터함
    if (_mplayer.state() == QProcess::Running && o == centralWidget())
    {
        // 키가 눌렸다면
        if (e->type() == QEvent::KeyPress)
        {
            // 키보드 이벤트로 캐스트
            QKeyEvent *ke = static_cast<QKeyEvent *>(e);

            // MPlayer 에 전달할 명령 문자열
            QString cmd;

            switch (ke->key())
            {
            case Qt::Key_Left:  // 왼쪽 화살표키: MPlayer 왼쪽키
                cmd = QString("key_down_event %1")
                        .arg(0x1000000 + 17/* Key_Left */);
                break;

            case Qt::Key_Right: // 오른쪽 화살표 키: MPlayer 오른쪽 키
                cmd = QString("key_down_event %1")
                        .arg(0x1000000 + 16/* Key_Right */);
                break;

            case Qt::Key_P: // P 키: 처음부터 다시 재생
                cmd = QString("loadfile %1").arg(_movieFilePath);
                break;

            case Qt::Key_S: // S 키: 재생 중지
                cmd = "stop";
                break;

            case Qt::Key_Space: // 스페이스 바: 일시정지
                cmd = "pause";
                break;
            }

            // 전달할 명령이 있을 때만
            if (!cmd.isEmpty())
            {
                // MPlayer 는 줄단위로 입력받음
                cmd += "\n";

                // MPlayer 에 전달
                _mplayer.write(cmd.toLocal8Bit());

                // 처리했음
                return true;
            }
        }
    }

    // 필터하지 않은 이벤트는 부모 클래스에 전달
    return QMainWindow::eventFilter(o, e);
}


10 번째 줄: 우리가 필터하고자 하는 객체가 맞는지 확인해야 한다. 이 프로젝트에서는 하나의 위젯만 필터하고 있지만, 여러 개의 객체를 필터하고 있다면 이 과정은 더욱 중요하다.

13 번째 줄: QEvent::type() 은 이벤트의 종류를 나타낸다. QEvent::QKeyPress 는 키가 눌릴 때 발생하는 이벤트이다.

15~44 번째 줄: 키보드 이벤트를 처리하기 위해서는 QEventQKeyEvent 로 캐스트해야 한다. 이 때, 키 종류는 QKeyEvent::key() 로 알 수 있다. 이 값은 Qt::Key_xxx 형태로 정의되어 있다. 자세한 것은 Assistant 를 보자.

주석에도 나와있지만, MPlayer 도 자체 키코드를 제공한다. 이 코드는 osdep/keycodes.h 에 정의되어 있다. 여러 개의 키코드를 사용해야 한다면, 해당 파일을 #include 해서 쓰는 것이 더 편할 것이다.

46~57 번째 줄: 특정 이벤트를 필터해서 다른 필터 또는 등록된 객체에 전달하고 싶지 않다면 true 를 반환하면 된다.

62 번째 줄: 다른 필터 또는 등록된 객체에 전달하고자 한다면 부모 클래스에 이벤트를 전달하면 된다.

3. 마무리하면서...

그렇게 길지 않은 코드로 MPlayer 의 간단한 front-end 를 만들어보았다. 이 프로젝트의 경우 간단한 제어만 했지만, MPlayer 의 출력에서 다양한 정보를 얻어 사용자에게 보여줄 수도 있고, MPlayer 를 제어하는 더 다양한 방법과 옵션을 제공할 수도 있다. 그럼에도 불구하고 기본틀이 크게 다르지는 않을 것이다.

MPlayer 가 제공하는 모든 명령어는 다음 명령을 통해 확인할 수 있다.

mplayer -input cmdlist

Slave 모드에 대한 더 자세한 내용은 MPlayer 소스의 DOCS/tech/slave.txt 에 설명되어 있으니 관심있는 사람은 꼭 보기 바란다.

다음은 <MPGui> 의 실행 모습이다.


전체 소스는 여기에서 확인하자.


댓글

이 블로그의 인기 게시물

토렌트: < 왕좌의 게임 > 시즌 1 ~ 시즌 8 완결편 마그넷

토렌트: < 스타워즈 > Ep.1 ~ Ep.6 마그넷

토렌트: NGC < 코스모스 > 우리말 더빙 전편(1편~13편) 마그넷