# 동영상 플레이어 #1 - 영상 출력

# 핵심 강의

동영상 준비 중

# 강의 개요

ffmpeg을 이용하여 간단한 동영상 플레이어를 만들기 전에 영상 출력 방법에 대해서 알아보겠습니다.

# 강의 전 준비 사항

# 이 강의에서 다룰 내용

  • WindowSDL 클래스: SDL2를 이용하여 영상을 화면에 표시
  • ffmpeg으로 영상(파일) 리소스 가져오기
    • http, ftp, rtmp, rtsp 등 다양한 프로토콜 지원
  • 비디오 스트림을 찾아내기
  • 비디오 코덱 오픈하기
  • 영상 리소스를 차례로 읽고 디코딩하여 화면에 출력하기

# WindowSDL

class WindowSDL {
public:
	/** 영상을 출력하기 위한 윈도우를 생성(오픈)합니다.
	@param caption 윈도우의 제목
	@param width 윈도우의 넓이 (가로 크기)
	@param height 윈도우의 높이 (세로 크기)
	*/
	bool open(string caption, int width, int height)

	/** 윈도우를 닫습니다. */
	void close()

	/** 32비트 BITMAP을 윈도우에 표시합니다.
	@param bitmap 표시할 BITMAP 데이터
	*/
	void showBitmap32(void* bitmap)

	/** YUV 포멧 이미지를 윈도우에 표시합니다. */
	void showYUV(const Uint8* y_plane, int y_pitch, const Uint8* u_plane, int u_pitch, const Uint8* v_plane, int v_pitch)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 전반적인 소스 구조

include ...

int main()
{
	// 비디오 소스(파일) 오픈

	// 비디오 스트림 찾기

	// 비디오 코덱 오픈

	// 비디오를 출력할 윈도우 오픈

	// 파일(비디오 소스) 반복해서 끝까지 읽기
	while (...) {
		// 비디오 디코딩
		// 디코딩된 영상을 화면에 표시
	}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

# include 및 전처리

#include <stdio.h>
#include <ryulib/sdl_window.hpp>

extern "C" {
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libavutil/imgutils.h>	
}
1
2
3
4
5
6
7
8
  • 2: 비디오 출력을 위한 헤더
  • 4-8: ffmpeg에서 필요한 헤더 파일, C로 작성된 라이브러리이기 때문에 extern "C"으로 감싸야 합니다. 이에 대해서는 "네임 맹글링 (Name mangling)"을 검색해보시기 바랍니다.

# main() 함수

# 비디오 소스(파일) 오픈

	//string filename = "D:/Work/test.mp4";
	string filename = "https://etc.s3.ap-northeast-2.amazonaws.com/AsomeIT.mp4";

	AVFormatContext* ctx_format = NULL;
	if (avformat_open_input(&ctx_format, filename.c_str(), NULL, NULL) != 0) return -1;
	if (avformat_find_stream_info(ctx_format, NULL) < 0) return -1;
1
2
3
4
5
6
  • 1-2: 사용할 영상 소스(파일)을 지정합니다. ffmpeg은 http, ftp, rtmp, rtsp 등 다양한 프로토콜을 지원합니다.
  • 5: 영상 소스를 오픈합니다. 에러가나면 프로그램을 종료합니다. 어느 부분에서 에러가 났는 지 알기 위해서 리턴 값을 달리하고 있습니다.
  • 6: 영상 소스에 스트림 정보가 있는 지 확인합니다. 만약 아무것도 없는 영상이면 프로그램을 종료합니다.

# 비디오 스트림 찾기

	int video_stream = -1;
	for (int i = 0; i < ctx_format->nb_streams; i++)
		if (ctx_format->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
			video_stream = i;
			break;
		}
	if (video_stream == -1) return -2;
1
2
3
4
5
6
7

영상의 스트림 목록을 순회하면서 비디오 타입이 발견되면 해당 순번을 video_stream에 저장하고 반복을 멈춘다.

# 비디오 코덱 오픈

	AVCodecParameters* parameters = ctx_format->streams[video_stream]->codecpar;
	AVCodec* codec = avcodec_find_decoder(parameters->codec_id);
	if (codec == NULL) return -3;
	AVCodecContext* ctx_codec = avcodec_alloc_context3(codec);
	if (avcodec_parameters_to_context(ctx_codec, parameters) != 0)  return -3;
	if (avcodec_open2(ctx_codec, codec, NULL) < 0) return -3;
1
2
3
4
5
6
  • 1: 스트림 객체에서 비디오 컨텍스트 객체를 가져옵니다.
  • 2: 컨텍스트의 codec_id에 맞는 코덱 객체를 가져옵니다.
  • 3: 코덱 컨텍스트 객체를 생성합니다.
  • 5: ctx_video에서 ctx_codec으로 필요한 정보(파라메터)를 복사합니다.
  • 6: 코덱을 사용할 수 있도록 오픈합니다. 위의 과정은 ffmpeg이 그렇게 설계되었을 뿐이기 때문에 굳이 과정 전체를 이해할 필요는 없습니다.

# 비디오를 출력할 윈도우 오픈

	WindowSDL window;
	window.open("ffmpeg", ctx_codec->width, ctx_codec->height);
1
2
  • 2: 영상을 출력할 윈도우를 오픈합니다.
    • 윈도우 타이틀(제목)에는 "ffmepg"이라고 표시합니다.
    • 윈도우 크기는 ctx_codec->width, ctx_codec->height로 지정합니다.

# 비디오 출력

	AVFrame* frame = av_frame_alloc();
	if (!frame) return -4;

	AVPacket packet;

	while (av_read_frame(ctx_format, &packet) >= 0) {
		// 비디오 스트림만 처리
		if (packet.stream_index == video_stream) {
			int ret = avcodec_send_packet(ctx_codec, &packet) < 0;
			if (ret < 0) {
				printf("Error sending a packet for decoding \n");
				return -5;
			}

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

				// 디코딩된 영상을 화면에 표시
				window.showYUV(frame->data[0], frame->linesize[0], frame->data[1], frame->linesize[1], frame->data[2], frame->linesize[2]);
			}
		}

		av_packet_unref(&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
  • 1: 디코딩 된 화면을 저장할 AVFrame 객체를 생성합니다.
  • 4: 영상 소스에서 읽어온 데이터를 저장할 AVPacket 객체를 생성합니다.
  • 6-30: 파일의 끝까지 반복하면서 (더 이상 데이터가 읽혀지지 않을 때까지) 패킷을 읽고 디코딩한 후 영상에 표시합니다.
    • 8: 이전에 찾아낸 비디오 스트림만 처리합니다.
    • 9: 디코더에게 읽어들인 데이터를 전송합니다.
    • 15-26: 디코딩이 끝난 데이터가 여러 프레임으로 구성되어 있을 수 있습니다. 그래서 프레임 수만큼 반복하면서 처리합니다.
      • 16: 디코딩이 끝난 데이터를 가져와서 화면에 표시할 준비를 합니다.
      • 25: 디코딩이 끝난 데이터를 화면에 표시합니다.

TIP

영상을 디코딩하면서 계속 표시만하기 때문에 영상이 빨리 감기처럼 빠르게 재생됩니다. 여기서는 전반적인 원리만을 설명하고 재생 시간을 제어나 종료처리 등은 생략하였습니다.

# 전체 코드

https://github.com/ryujt/ff-player/blob/master/src/decoding-video/decoding-video/decoding-video.cpp 참고