본문 바로가기
놀기/Qt

ffmpeg을 활용한 transcoder 만들기 (QProcess, 정규식 활용)

by Hi~ 2022. 5. 6.

목차

     

     

    0. 들어가기

    요즘이야 대부분의 동영상/오디오를 한 장치에서 재생할 수 있지만, 10여 년 전만 해도 그렇지 않았다. 특정 코덱이 한정된 칩을 사용했고 S/W로 디코딩할 수 있는 한계가 있었다. 그래서 Bitrate를 낮추거나 Frame을 변경하거나 포맷/코덱을 변환해야 하는 일이 많았다. 요즘은 굳이 변환해야 할 일이 없지만 그래도 변환이 필요한 경우가 있다. 이럴 때 ffmpeg이 유용하다. ffmpeg이 오픈소스이니 잘 이용해 자신의 프로그램에 넣는 것도 좋은 방법이지만, 굳이 그렇게 시간을 쓰지 않아도 가능은 하다. 포맷/코덱 등과 같은 정보를 입력할 수 있는 인터페이스를 만들어 그 옵션을 ffmpeg 실행 시에 넣어주면 된다. 마침 인터넷을 찾아보니 만들어진 샘플이 있었다. 다만, 진행 상태를 Progress Bar로 보여주면 좋을 것 같아 수정을 하기로 했다.

     

     

    1. FFmpeg Dialog 샘플 코드

    인터넷에서 찾은 샘플이다. UI는 아래와 같이 간단하며 입력 파일과 출력 파일을 지정하여 변환을 할 수 있다. 별다른 옵션을 넣지 않았고 확장자 정보로 포맷 정도를 바꾸는 형태다. 아래 있는 Play 버튼은 입력/출력 동영상으로 ffplay를 사용해서 재생하는 버튼이다.

     

    기본적인 설명은 아래 사이트에서 확인할 수 있다.

    https://www.bogotobogo.com/Qt/Qt5_QProcess_QFileDialog_QTextEdit_FFmpeg.php

     

    Qt5 Tutorial FFmpeg Converter using QProcess - 2020

    Ph.D. / Golden Gate Ave, San Francisco / Seoul National Univ / Carnegie Mellon / UC Berkeley / DevOps / Deep Learning / Visualization

    www.bogotobogo.com

     

    위의 사이트에 포함된 소스코드다.

    https://www.bogotobogo.com/Qt/images/FFmpeg_QProcess/

     

    Index of /Qt/images/FFmpeg_QProcess

     

    www.bogotobogo.com

     

     

    2. ffmpeg 출력 메시지 분석

    ffmpeg을 사용하여 콘텐츠 변환 시, 원본 및 결과물에 대한 비디오/오디오 등의 포맷 및 코덱에 대한 정보가 출력된다. 상당히 유용한 정보가 포함되어 있어 잘 활용하면 쓸모가 많다.

     

    QProcess에서 제공하는 signal을 이용해서 아래와 같이 표준 출력 메시지를 받을 수 있다. 문제는 여러 라인이 한 번에 들어오고 개행 방식이 곳곳이 달라 정리를 해야 한다. carrige return (\r)을 모두 line feed (\n)로 변경하고 line feed를 기준으로 자른 후 빈 줄을 제외시켜 사용한다.

     

    std::vector<std::string> convToLineByLine(std::string s) {
        std::vector<std::string> result;
        boost::replace_all(s, "\r", "\n");
        std::vector<std::string> ss = split(s, '\n');
        for (auto & i : ss) {
            if (i.length() > 0) {
                static int lineNum = 0;
                std::cerr << lineNum++ << " : " << i << std::endl;
                result.push_back(i);
            }
        }
    
        return result;
    }
    void Dialog::readyReadStandardOutput()
    {
        mOutputString = mTranscodingProcess->readAllStandardOutput();
        std::vector<std::string> outputStrings = convToLineByLine(mOutputString.toStdString());
    
        for (auto & s : outputStrings) {
            if (s.length() > 0) {
                if (mDuration < 0) {
                    mDuration = getDuration(s);
                    if (mDuration > 0) {
                        ui->lineEdit->setText(QString::number(mDuration));
                    }
                    else {
                        // 전체 시간이 확인되어야만 다음으로 진행...
                        continue;
                    }
                }
    
                long pos = getCurrentPos(s);
                if (pos > 0) {
                    int prog = (pos*100) / mDuration;
                    ui->progressBar->setValue(prog);
                }
            }
        }
    }

     

    이와 같이 준비가 되었다면 이제 본격적으로 들어가자.

     

    먼저, 재생 시간이다. 진행상황을 표시하려면 전체 길이를 알아야 한다. 비디오와 오디오의 길이가 다를 수 있지만, 대략 비디오 길이에 맞춰 진행률을 표시하면 되므로 15라인의 정보를 사용할 계획이다.

     

    진행률을 표시하기 위해서는 현재 어느 위치에 있는지에 대한 정보가 있어야 한다. ffmpeg에서는 변환 과정에 대해 아래와 같이 정보를 표시해 준다. 재생 길이를 기준으로 하므로 time 정보를 사용할 계획이다.

     

     

    3. 재생 시간, 변환 위치 정보 추출

    1) 재생 시간 정보

     

      Duration: 00:00:04.68, start: 0.000000, bitrate: 1626 kb/s

    재생 시간은 공백 2개와 Duration 단어로 시작한다. 

     

    bool hasDurationInfo(std::string s) {
        boost::regex expr{ "^  Duration: (\\d){2}:(\\d){2}:(\\d){2}.(\\d){2},(\\s)*start" };
        boost::smatch what;
    
        if (boost::regex_search(s, what, expr)) {
            std::cerr << "Found duration info." << std::endl;
            return true;
        }
    
        return false;
    }

    "  Duration:"으로 XX:XX:XX.XX, start: 형식의 문자열이 나오는지를 확인하기 위해서는 위와 같이 정규식을 만들어 검색하면 된다. 이렇게 검색하면 "  Duration: 00:00:04.68, start"의 값을 얻을 수 있다. 이 중 시간 정보만 필요한데 콜론을 제거할 수 없어 어쩔 수 없이 다시 한번 검색을 한 후에 시간 정보를 얻는다.

     

    long getDuration(std::string s) {
        boost::regex expr{ " (\\d){2}:(\\d){2}:(\\d){2}.(\\d){2},(\\s)*start" };
        boost::smatch what;
        long duration = -1;
    
        if (hasDurationInfo(s)) {
            if (boost::regex_search(s, what, expr)) {
                std::string str = what[0];
                boost::regex re("[a-zA-Z=, ]*");
                std::string result = boost::regex_replace(str, re, "");
    
                duration = convToMilliseconds(result);
            }
        }
    
        return duration;
    }

    정규식을 잘 몰라 두 단계로 했는데 방법은 여러 가지이니 다들 알아서 잘하시길... 

     

     

     

    2) 변환 위치 정보

     

    frame=   46 fps=0.0 q=0.0 size=       0kB time=00:00:01.71 bitrate=   0.2kbits/s dup=1 drop=0 speed=2.71x

    변환 위치에 대한 정보는 frame으로 시작하는 위의 정보다.

     

    long getCurrentPos(std::string s) {
        boost::regex expr{ "time=(\\d){2}:(\\d){2}:(\\d){2}.(\\d){2}(\\s)*bitrate=" };
        boost::smatch what;
        long pos = -1;
    
        if (boost::regex_search(s, what, expr)) {
            std::string str = what[0];
            boost::regex re("[a-zA-Z= ]*");
            std::string result = boost::regex_replace(str, re, "");
    
            pos = convToMilliseconds(result);
        }
    
        return pos;
    }

    frame으로 시작한다는 내용은 빼고 간단하게 "time=00:00:01.71 bitrate=" 부분을 찾는다. 이 중 필요한 부분은 "00:00:01.71"이다. 이 값을 제외한 나머지 부분을 boost::regex_replace()를 사용하여 지운다.

     

     

    3) 시간 정보를 millisecond로 변환

     

    XX:XX:XX.XX 형식의 문자열 시간 정보는 그냥 사용할 수가 없다. 아래와 같이 millisecond(long type)으로 변환한다.

     

    long convToMilliseconds(std::string s) {
        const boost::posix_time::ptime time = boost::posix_time::time_from_string("1970-01-01 " + s);
        return time.time_of_day().total_milliseconds();
    }

     

    이와 같은 방식을 사용하면 아래와 같이 Progress bar를 사용하여 표시할 수 있다. 다만 Duration과 현재 위치 정보의 끝 부분이 차이가 있을 수 있다. 할 수 없이 변환이 완료되었을 때, Progress bar를 100%로 설정했다.

     

    수정한 다이얼로그 화면

     

    변환 완료 여부는 그냥 원본 코드를 그대로 사용했는데, 에러 출력 메시지 또는 ffmpeg의 반환 값을 사용하여 처리해야 하는데 다음에 시간이 되면 해보는 것으로...

     

     

     

    4. 마무리

    c++/boost/정규식을 잘 사용하지 않아 관련 전문가가 보기에는 쓰레기 코드처럼 보이겠지만, 뭐 돌아가면 되는 것 아니겠는가. 쓰다 보면 코드가 깔끔해지고 능숙해지겠지.

    어쨌든 정규식에 대한 정보는 인터넷에 많다. 다만 활용을 하려면 막상 어렵다. 특히나 나 같은 무식이에게는 ㅎㅎ. 단순히 책만 보는 것이 해답은 아닐 것 같고 허접하지만 이렇게 실습하는 것이 느리지만 빠른 길이 아닐까 한다. 

     

    dialog.cpp
    0.01MB
    dialog.h
    0.00MB
    dialog.ui
    0.01MB
    ffmpeg.pro
    0.00MB
    main.cpp
    0.00MB

     

     

     

     

    댓글