본문 바로가기
잡(job)기술/ffmpeg 라이브러리

Remux 클래스로 전환

by 무니이구나 2022. 4. 6.

드디어, 본격적인 C++ 프로그래밍을 할 차례이다. 우선 리소스 할당 문제부터 정리할 필요가 있다. remux() 함수는 goto 문을 이용해서 깔끔하게 리소스 문제를 해결하고 있다. 그러나, goto로 인한 제어루틴의 변경은 코드 따라가기를 상당히 힘들게 한다. remux 과정을 클래스로 묶음으로써 리소스 해제를 좀 더 C++스럽게 완료할 수 있다. remux() 함수를 Remux 라는 클래스로 포팅하는 과정을 이 글에서 정리한다. 지난 번 글의 소스 코드에서 시작한다.

https://craftsmanship.tistory.com/110

 

명령 라인 처리 옮기기

이번에는 지난 번 글에 이어서, 명령 라인 처리를 옮기는 과정만 기록한다. 역시 짧은 글이 될 것 같다. https://craftsmanship.tistory.com/109 remuxing.c 를 c++로 wrapping하기 이번 글은 굉장히 간단할 것 같..

craftsmanship.tistory.com

Remux 클래스의 인터페이스

우선 Remux 클래스의 인터페이스와 멤버는 다음과 같이 구성했다.

class Remux {
    char const* in_filename;
    char const* out_filename;
    AVOutputFormat *ofmt = NULL;
    AVFormatContext *ifmt_ctx = NULL, *ofmt_ctx = NULL;
    AVPacket pkt;
    int ret, i;
    int stream_index = 0;
    int *stream_mapping = NULL;
    int stream_mapping_size = 0;

public:
    Remux(char const* in_filename, char const* out_filename);
    void run();
    ~Remux();

private:
    void init();
};

remux() 함수의 내부 변수를 그대로 멤버로 선언하고, 인자로 받는 부분을 Remux 생성자의 인자로 사용해서, 이 값이 유지하고 있을 in_filename, out_filename 변수를 추가했다.

외부에서는 Remux 인스턴스를 생성하고, run()을 실행하게 되며, 끝날 때 소멸자가 호출되는 형태로 과정을 예상할 수 있다.

Remux 생성자

생성자에서는 별다르게 하는 일은 없다. 인자로 in_filename, out_filename 을 받아서, 멤버 변수로 설정해준다.

Remux init 함수

이 함수는 private 함수인데, 자원들을 할당하는 역할을 한다. 실제로 이 함수는 run()에서 호출하도록 되어 있다. 생성자에서 이 작업을 하지 않고, 여기에서 하는 이유는, 자원 할당을 하면서 오류가 발생했을 때, 이를 처리하기 위해서 예외를 발생시킬텐데, 이것을 생성자에서 하게 되면, 생성자가 완료되지 않은 상태에서 스코프를 벗어나, 소멸자가 호출되지 않기 때문이다. 생성자에서 예외를 발생시키지 마라는 글들을 봐 왔는데, 이번에야 비로소 그 이유를 체감하게 되는 것 같다.

void Remux::init() {
    if ((ret = avformat_open_input(&ifmt_ctx, in_filename, 0, 0)) < 0)    {
        std::stringstream ss;
        ss << "Could not open input file " << in_filename;
        throw std::runtime_error(ss.str());
    }

    if ((ret = avformat_find_stream_info(ifmt_ctx, 0)) < 0)    {
        throw std::runtime_error("Failed to retrieve input stream information");
    }
    ...
}

Remux run 함수

init 함수를 호출하고, 헤더 쓰기, remux한 패킷 생성, trailer 로 마무리하기 과정을 실행한다. 실제 remux 기능을 여기에서 한다고 보면 된다. init 함수와 마찬가지로 ffmpeg 라이브러리 API 실행 중에 에러가 리턴되면 예외를 발생시키도록 대응을 했다.

void Remux::run() {
    init();

    ret = avformat_write_header(ofmt_ctx, NULL);
    if (ret < 0)    {
        throw std::runtime_error("Error occurred when opening output file");
    }

    while (1)
    {
        AVStream *in_stream, *out_stream;

        ret = av_read_frame(ifmt_ctx, &pkt);
        if (ret < 0)
            throw std::runtime_error("Can not read frame");
	...
}

Remux 소멸자

이번에 remux 함수를 Remux 클래스로 변환하게 된 가장 큰 이유를 가지고 있는 부분이기도 하다. 예외가 발생하고 관련 오류를 표시하는 처리를 작업을 하겠지만, 미리 할당한 리소스는 소멸자가 호출되면서 자동으로 해제되도록 동작하게 된다.

Remux::~Remux() {
    avformat_close_input(&ifmt_ctx);

    /* close output */
    if (ofmt_ctx && !(ofmt->flags & AVFMT_NOFILE))
        avio_closep(&ofmt_ctx->pb);
    avformat_free_context(ofmt_ctx);

    av_freep(&stream_mapping);

    if (ret < 0 && ret != AVERROR_EOF)
    {
        char errorString[AV_ERROR_MAX_STRING_SIZE];
        av_make_error_string(errorString, AV_ERROR_MAX_STRING_SIZE, ret);
        std::stringstream ss;
        ss << "Error occurred: " << errorString;
        std::cerr << ss.str() << std::endl;
    }
}

main 함수

최종적으로 main 함수에서는 remux 함수를 호출하는 대신, Remux 객체를 생성하고 run 멤버 함수를 실행한다. 그리고, 예외가 발생했을 때 이를 캐치해서 오류를 표시한다. 리소스 해제는 객체의 스코프가 벗어날 때 자동으로 일어난다.

int main(int argc, char* argv[])    {
	...

    try {
        Remux remux{argv[1], argv[2]};
        remux.run();
    } catch(std::exception& e) {
        std::cerr << e.what();
        return 1;
    }

    return 0;
}

소스 저장소

작성한 코드는 깃헙에 업데이트한다.

https://github.com/moonyl/rtsp-to-web-live/tree/auto_release

 

GitHub - moonyl/rtsp-to-web-live

Contribute to moonyl/rtsp-to-web-live development by creating an account on GitHub.

github.com