# 동영상 플레이어 #4 - 구현계층 코딩 및 기본동작 확인

# 핵심 강의

# 강의 개요

이번 강의에서는 본격적으로 ffmpeg을 이용하여 만드는 간단한 동영상 플레이어의 기본 동작을 구현하겠습니다.

동영상 파일 닫기, 에러 및 종료 처리 등은 다음 강의에서 다루도록 하겠습니다.

# 강의 전 준비 사항

# 이 강의에서 다룰 내용

  • 논리계층에서 작성된 FFStream, FFAudio, FFVideo의 인터페이스를 토대로 코드 구현
  • ffmpeg을 이용하여 동영상 파일을 열고 패킷을 가져오는 방법
  • ffmpeg을 이용하여 오디오 및 비디오 디코딩 후 출력하는 방법

# 실행결과

  • 프로그램을 실행하고 "p"를 입력 후 엔터를 치면 동영상이 재생되고, "a"를 입력 후 엔터를 치면 재생이 잠시 멈추게 됩니다.

# 구현계층 소스

# FFStream

#pragma once

extern "C" {
#include <libavformat/avformat.h>
}

using namespace std;

class FFStream {
public:
	bool open(string filename)
	{
		if (avformat_open_input(&context_, filename.c_str(), NULL, NULL) != 0) return false;
		if (avformat_find_stream_info(context_, NULL) < 0) return false;
		return true;
	}

	void close()
	{
	}

	void play()
	{
		is_playing_ = true;
	}

	void pause()
	{
		is_playing_ = false;
	}

	bool isPlaying() { return is_playing_; }

	AVPacket* read()
	{
		AVPacket* packet = av_packet_alloc();
		if (av_read_frame(context_, packet) < 0) {
			av_packet_free(&packet);
			return nullptr;
		}
		return packet;
	}

	AVFormatContext* getContext()
	{
		return context_;
	}

private:
	AVFormatContext* context_ = nullptr;
	bool is_playing_ = false;
};
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
  • 3: ffmpeg은 C로 작성된 라이브러리입니다. 따라서 extern "C"로 감싸줍니다. 이는 C++과 C가 컴파일 시 사용하는 함수이름의 규칙이 다르기 때문입니다.
  • 13: 동영상 파일을 열어서 context_ 변수에 정보를 넣습니다. 열 수 없는 파일이면 false를 리턴합니다.
  • 14: 동영상 안에 스트림(오디오나 영상 데이터)가 없으면 false를 리턴합니다.
  • 36: 동영상 파일에서 패킷을 읽어오기 위하여 AVPacket을 메모리에 할당합니다.
  • 37: 패킷을 하나 읽어옵니다. 없으면 nullptr을 리턴합니다.
  • 41: 읽어 온 패킷을 리턴합니다.

# FFAudio

#pragma once

#include <ryulib/Worker.hpp>
#include <ryulib/sdl_audio.hpp>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswresample/swresample.h>
}

class FFAudio {
public:
	FFAudio()
	{
		frame = av_frame_alloc();
		reframe = av_frame_alloc();

		worker_.setOnTask([&](int task, const string text, const void* data, int size, int tag){
			decode_and_play((AVPacket*) data);
		});
	}

	bool open(AVFormatContext* context)
	{
		for (int i = 0; i < context->nb_streams; i++)
			if (context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
				stream_index_ = i;
				break;
			}
		if (stream_index_ == -1) {
			printf("FFAudio.open - stream_ == -1 \n");
			return false;
		}

		parameters_ = context->streams[stream_index_]->codecpar;
		codec_ = avcodec_find_decoder(parameters_->codec_id);
		if (codec_ == NULL) {
			printf("FFAudio.open - codec == NULL \n");
			return false;
		}

		context_ = avcodec_alloc_context3(codec_);
		if (avcodec_parameters_to_context(context_, parameters_) != 0) 
		{
			printf("FFAudio.open - avcodec_parameters_to_context \n");
			return false;
		}

		if (avcodec_open2(context_, codec_, NULL) < 0) {
			printf("FFAudio.open - avcodec_open2 \n");
			return false;
		}

		swr_ = swr_alloc_set_opts(
			NULL,
			context_->channel_layout,
			AV_SAMPLE_FMT_FLT,
			context_->sample_rate,
			context_->channel_layout,
			(AVSampleFormat) parameters_->format,
			context_->sample_rate,
			0,
			NULL
		);
		swr_init(swr_);

		return audio_.open(context_->channels, context_->sample_rate, 1024);
	}

	void close()
	{
	}

	void write(AVPacket* packet)
	{
		worker_.add(0, packet);
	}

	int getStreamIndex() { return stream_index_; }

	bool isEmpty() { return audio_.getDelayCount() < 2; }

private:
	int stream_index_ = -1;
	AVCodecParameters* parameters_ = nullptr;
	AVCodecContext* context_ = nullptr;
	AVCodec* codec_ = nullptr;
	Worker worker_;
	AudioSDL audio_;
	SwrContext* swr_;
	AVFrame* frame = nullptr;
	AVFrame* reframe = nullptr;

	void decode_and_play(AVPacket* packet)
	{
		int ret = avcodec_send_packet(context_, packet) < 0;
		if (ret < 0) {
			printf("FFAudio - Error sending a packet for decoding \n");
			return;
		}	

		while (ret >= 0) {
			ret = avcodec_receive_frame(context_, frame);
			if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
				break;
			} else if (ret < 0) {
				printf("Error sending a packet for decoding \n");
				return;
			}

			// 포멧 변환
			reframe->channel_layout = frame->channel_layout;
			reframe->sample_rate = frame->sample_rate;
			reframe->format = AV_SAMPLE_FMT_FLT;
			int ret = swr_convert_frame(swr_, reframe, frame);

			int data_size = av_samples_get_buffer_size(NULL, context_->channels, frame->nb_samples, (AVSampleFormat) reframe->format, 0);
			audio_.play(reframe->data[0], data_size);
		}

		av_packet_free(&packet);
	}
};
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
  • 14-22: 생성자에서 초기화를 진행합니다.
    • 16: 디코딩 된 데이터를 저장할 구조체에 메모리를 할당합니다.
    • 17: 오디오 포멧을 출력 장치에 맞춰서 리샘플링 하기 위하여 reframe에도 메모리를 할당합니다.
    • 19-21: Worker 클래스를 이용하여 패킷이 들어오면 별도의 스레드를 이용해서 디코딩을 진행합니다. 자세한 내용은 http://10bun.tv/beginner/episode-2/#worker (opens new window)을 참고하세요.
  • 26-34: 동영상 정보 안에서 오디오 관련 스트림을 찾아냅니다. 오디오가 여러 개 있는 동영상의 경우에는 첫 번 째 스트림만 사용하고 나머지는 무시됩니다.
  • 36-41: 오디오 코덱 정보를 가져옵니다.
  • 43-53: 오디오 코덱을 준비합니다.
  • 55-66: 동영상의 오디오를 출력장치에 맞게 리샘플링 할 수 있도록 준비합니다.
  • 68: SDL을 이용하여 오디오를 출력할 수 있는 장치를 준비합니다. 1024는 한 번에 처리 할 수 있는 프레임 개수입니다.
  • 75-78: 외부에서 패킷을 전달받으면 Worker에게 처리해달라고 요청합니다. 19-21: 라인의 코드가 별도의 스레드로 동작하면서 디코딩을 시작합니다.
  • 95-123: 디코딩을 하는 메소드입니다.
    • 97-101: 디코더에게 패킷을 전달합니다.
    • 104-110: 디코딩 된 프레임을 가져옵니다.
    • 112-116: 출력 장치에 맞도록 오디오를 리샘플링합니다.
    • 118-119: 리샘플링 된 데이터의 크기를 구하고 해당 크기만큼의 오디오 데이터를 출력장치(audio_)에게 재생하도록 요청합니다.

# FFVideo

#pragma once

#include <ryulib/Worker.hpp>
#include <ryulib/sdl_window.hpp>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>	
}

class FFVideo {
public:
	FFVideo()
	{
		frame = av_frame_alloc();

		worker_.setOnTask([&](int task, const string text, const void* data, int size, int tag){
			decode_and_play((AVPacket*) data);
		});
	}

	bool open(AVFormatContext* context)
	{
		for (int i = 0; i < context->nb_streams; i++)
			if (context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
				stream_index_ = i;
				break;
			}
		if (stream_index_ == -1) {
			printf("FFVideo.open - stream_ == -1 \n");
			return false;
		}

		parameters_ = context->streams[stream_index_]->codecpar;
		codec_ = avcodec_find_decoder(parameters_->codec_id);
		if (codec_ == NULL) {
			printf("FFVideo.open - codec == NULL \n");
			return false;
		}
		
		context_ = avcodec_alloc_context3(codec_);
		if (avcodec_parameters_to_context(context_, parameters_) != 0) 
		{
			printf("FFVideo.open - avcodec_parameters_to_context \n");
			return false;
		}

		if (avcodec_open2(context_, codec_, NULL) < 0) {
			printf("FFVideo.open - avcodec_open2 \n");
			return false;
		}

		video_.open("ffplayer", context_->width, context_->height);

		return true;
	}

	void close()
	{
	}

	void write(AVPacket* packet)
	{
		worker_.add(0, packet);
	}

	int getStreamIndex() { return stream_index_; }

	bool isEmpty()
	{
		return true;
	}

private:
	int stream_index_ = -1;
	AVCodecParameters* parameters_ = nullptr;
	AVCodecContext* context_ = nullptr;
	AVCodec* codec_ = nullptr;
	Worker worker_;
	WindowSDL video_;
	AVFrame* frame = nullptr;

	void decode_and_play(AVPacket* packet)
	{
		int ret = avcodec_send_packet(context_, packet) < 0;
		if (ret < 0) {
			printf("FFVideo - Error sending a packet for decoding \n");
			return;
		}	

		while (ret >= 0) {
			ret = avcodec_receive_frame(context_, frame);
			if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF) {
				break;
			} else if (ret < 0) {
				printf("Error sending a packet for decoding \n");
				return;
			}

			video_.showYUV(frame->data[0], frame->linesize[0], frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]);
		}

		av_packet_free(&packet);
	}

};
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
  • 14-21: 생성자에서 초기화를 진행합니다.
    • 16: 디코딩 된 데이터를 저장할 구조체에 메모리를 할당합니다.
    • 18-20: Worker 클래스를 이용하여 패킷이 들어오면 별도의 스레드를 이용해서 디코딩을 진행합니다. 자세한 내용은 http://10bun.tv/beginner/episode-2/#worker (opens new window)을 참고하세요.
  • 25-33: 동영상 정보 안에서 비디오 관련 스트림을 찾아냅니다. 비디오가 여러 개 있는 동영상의 경우에는 첫 번 째 스트림만 사용하고 나머지는 무시됩니다.
  • 35-40: 비디오 코덱 정보를 가져옵니다.
  • 42-52: 비디오 코덱을 준비합니다.
  • 54: SDL을 이용하여 비디오를 출력할 수 있는 장치를 준비합니다.
  • 63-66: 외부에서 패킷을 전달받으면 Worker에게 처리해달라고 요청합니다. 10-20: 라인의 코드가 별도의 스레드로 동작하면서 디코딩을 시작합니다.
  • 84-105: 디코딩을 하는 메소드입니다.
    • 86-90: 디코더에게 패킷을 전달합니다.
    • 93-99: 디코딩 된 프레임을 가져옵니다.
    • 101: 비디오 데이터를 출력장치(video_)에게 재생하도록 요청합니다.