buntalk.com
FFmpeg and SDL Tutorial - Spawning Threads 본문
반응형
2019/04/08
Tutorial 04: Spawning Threads
오버뷰
마지막에 SDL의 오디오 함수를 통해 오디오 지원을 추가했다. SDL은 오디오가 필요할 때 우리가 지정한 콜백함수를 호출할 수 있는 스레드를 생성한다. 이제 비디오 디스플레이에 대한 처리를 수행할 때다. 이는 코드를 더욱 모듈러하고 작업하기 쉽게 하는데 특히 싱킹을 추가하기를 원할 때 그렇다. 그래서 어디서 시작해야할까?
먼저 메인함수는 너무 많은 것을 다루고 있음을 알 수 있다. 이벤트 루프를 거쳐 실행하며, 패킷을 읽고, 비디오를 디코딩한다. 그래서 우리가 할 것은 이들을 부분으로 쪼개는 것이다. 스레드를 가져 패킷을 디코딩하는 것을 맡긴다. 이들 패킷은 큐에 저장하고 연관된 오디오와 비디오 스레드에서 읽어들인다. 오디오 스레드는 우리가 원하는 그대로이지만, 비디오 스레드는 비디오를 자체적으로 표시해야 하므로 조금 복잡하다. 메인 루프에 여전히 실제 표시 코드를 추가할 것이다. 그러나 매번 그저 보여주는 것이아닌 비디오 디스플레이와 이벤트 루프를 결합할 것이다. 이 아이디어는 비디오를 디코드하기 위한 것이고 다른 큐에 결과 프레임을 저장하며 그러면 커스텀 이벤트를 생성한다. (FF_REFRESH_EVENT) 이는 이벤트시스템에 추가하여 이벤트 루프가 그것을 보고 큐에서 다음 프레임을 보여준다. 이를 나타내면 다음과 같다.
________ audio _______ _____
| | pkts | | | | to spkr
| DECODE |----->| AUDIO |--->| SDL |-->
|________| |_______| |_____|
| video _______
| pkts | |
+---------->| VIDEO |
________ |_______| _______
| | | | |
| EVENT | +------>| VIDEO | to mon.
| LOOP |----------------->| DISP. |-->
|_______|<---FF_REFRESH----|_______|
이벤트 루프를 통해 비디오를 디스플레이를 제어하는 흐름의 주요 목적은 SDL_Delay 스레드를 사용하는 것인데, 스크린 상에 비디오를 보여줄 때를 정확하게 제어할 수 있다. 그러면 최종적으로 다음 튜토리얼에서 비디오를 싱크하고 다음 비디오 리프레쉬를 스케쥴할 작은 코드만 추가하면 적절한 때에 적절한 그림이 보여지게 된다.
코드 단순화
또한 코드를 단순화 한다. 이 오디오와 비디오 코덱 정보를 가지고 큐와 버퍼를 추가할 것이고 그리고 추가적인게 있을 수 있기 때문이다. 이 모든 작업은 하나의 논리 단위를 위한 것 viz 영화이다. 그러므로 VideoState라 불리는 큰 정보를 담는 단일 구조체를 만든다.
typedef struct VideoState {
AVFormatContext* pFormatCtx;
int videoStream, audioStream;
AVStream* audio_st;
AVCodecContex* audio_ctx;
PacketQueue audioq;
uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE*3)/2];
unsigned int audio_buf_size;
unsigned int audio_buf_index;
AVPacket audio_pkt;
uint8_t* audio_pkt_data;
int audio_pkt_size;
AVStream* video_st;
AVCodecContext* video_ctx;
PacketQueue videoq;
VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];
int pictq_size, pictq_rindex, pictq_windex;
SDL_mutex* pictq_mutex;
SDL_cond* pictq_cond;
SDL_Thread* parse_tid;
SDL_Thread* video_tid;
char filename[1024];
int quit;
} VideoState;
우리가 어디로 갈지 잠시 살펴보자. 기본 정보를 본다. 포맷 컨텍스트와 오디오와 비디오 스트림 인덱스이다. 그리고 연관 AVStream객체이다. 그러면 이들 오디오 버퍼의 몇몇을 이 구조체로 옮겼다. 이들 (audio_buf, audio_buf_size, 등) 은 오디오에대한 모든 정보로서 여전히 주위에 위치한다. 비디오에 대한 추가적인 큐를 추가했고, 디코드된 프레임(오버레이에 제공될)을 위한 버퍼(큐로 사용될 이를 위한 팬시한 큐잉은 필요치 않다)도있다. VideoPicture 구조체는 직접 만들 것이다. (곧 살펴보게 된다.) 두개의 추가 스레드를 위한 할당한 포인터 그리고 종료 플래그 그리고 영화의 파일이름이다.
그러면 메인함수로 돌아가 어떻게 변화시킬지 보자. VideoState구조체를 설정한다.
int main(int argc, char* argv[]) {
SDL_Event event;
VideoState* is;
is = av_mallocz(sizeof(VideoState));
av_mallocz() 는 메모리할당에 편리한 함수로서 0으로 채운다.
디스플레이 버퍼를 위해 락을 초기화하는데 이벤트 루프가 디스플레이 함수를 호출하기 때문이다. - 디스플레이함수는 pictq 로 부터 미리 디코드된 프레임을 당길 것이다. 동시에 우리의 비디오 디코더는 거기에 정보를 설정하며 우리는 어떤 것에 먼저 갈지는 모른다. 희망적으로 이 것이 고전적 레이스 컨데션임을 안다. 그러므로 스레드를 시작하기 전에 먼저 할당한다. 영화의 파일 이름을 VideoState에 복사한다.
av_strlcpy(is->filename, argv[1], sizeof(is->filename));
is->pictq_mutex = SDL_CreateMutex();
is->pictq_cond = SDL_CreateCond();
av_strlcpy 는 FFmpeg함수로서 strncpy 에 추가적인 바운드 체크를 한다.
우리의 첫 스레드
이제 스레드를 만들고 실제 작업을 수행한다.
schedule_refresh(is, 40);
is->parse_tid = SDL_CreateThread(decode_thread, is);
if (!is->parse_tid) {
af_free(is);
return -1;
}
schedule_refresh 는 함수로서 이후에 정의할 것이다. 이 것이 기본적으로 수행하는 것은 지정된 밀리세컨드 이후에 시스템에 FF_REFRESH_EVENT를 건내는 것이다. 이는 이벤트 큐에 있을 때 비디오 새로고침 함수를 호출한다. 하지만 지금은 SDL_CreateThread() 를 살펴보는 것이다.
SDL_CreateThread() 는 새로운 스레드를 파생하고 원래의 처리의 모든 메모리에 완전히 접근하는 함수상에 실행되는 스레드를 시작한다. 이는 또한 그 함수에 사용자 정의 데이터를 전달한다. 이 경우 VideoState 구조체가 붙여진 decode_thread() 를 호출한다. 함수의 처음 반은 새로울 것 없다; 간단히 파일을 열고 오디오와 비디오 스트림 인덱스를 찾는다. 우리가 하는 다른 것은 큰 구조체 내에 포맷을 저장하는 것이다. 스트림 인덱스를 찾은 후 우리가 지정한 다른 함수를 호출한다. stream_component_open(). 이 것은 분할을 위한 꽤 자연스러운 방법이고 비디오와 오디오 코덱을 설정하는 비슷하므로 이 함수의 일부는 재사용한다.
stream_component_open() 함수는 코덱 디코더를 찾고, 오디오 옵션을 설정하고 중요한 정보를 큰 구조체에 저장하고 오디오와 비디오 스레드를 생성한다. 여기에는 추가적인 옵션으로 자동검출이 아닌 코덱강제 같은 것이 있을 수 있다.
int stream_component_open(VideoState* is, int stream_index) {
AVFormatContext* pFormatCtx = is->pFormatCtx;
AVCodecContext* codecCtx;
AVCodec* codec;
SDL_AudioSpec wanted_spec, spec;
if (stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
return -1;
}
codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);
if (!codec) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
codecCtx = avcodec_alloc_context3(codec);
if (avcodec_copy_context(codecCtx, pFormatCtx->streams[stream_index]->codec) != 0) {
fprintf(stderr, "Couldn't copy codec context");
return -1; // 코덱 컨텍스트 복사 에러
}
if (codecCtx->codec_type == AMMEDIA_TYPE_AUDIO) {
// 코덱 인포에서 오디오 설정
wanted_spec.freq = codecCtx->sample_rate;
/* ... etc ... */
wanted_spec.callback = audio_callback;
wanted_spec.userdata = is;
if (SDL_OpenAudio(&wanted_spec, &spec) < 0) {
fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;
}
}
if (avcodec_open2(codecCtx, codec, NULL) < 0) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
switch (codecCtx->codec_type) {
case AMMEDIA_TYPE_AUDIO:
is->audioStream = stream_index;
is->audio_st = pFormatCtx->streams[stream_index];
is->audio_ctx = codecCtx;
is->audio_buf_size = 0;
is->audio_buf_index = 0;
memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
packet_queue_init(&is->audioq);
SDL_PauseAudio(0);
break;
case AMMEDIA_TYPE_VIDEO:
is->videoStream = stream_index;
is->video_st = pFormatCtx->streams[stream_index];
is->video_ctx = codecCtx;
packet_queue_init(&is->videoq);
is->video_tid = SDL_CreateThread(video_thread, is);
is->sws_ctx = sws_getContext(is->video_st->codec->width, is->video_st->codec->height, is->video_st->codec->pix_fmt, is->video_st->codec->width, is->video_st->codec->height, PIX_FMT_YUV420P, SWS_BILINEAR, NULL, NULL, NULL);
break;
default:
break;
}
}
오디오 비디오를 위해 일반화되었다는 점만 제외하고 이 전에 한 것과 거의 같다. aCodecCtx대신 오디오 콜백을 위해 사용자 데이터로 큰 구조체에 할당했음을 기억하자. 스트림 자체를 audio_st, video_st 로 저장했다. 비디오 큐를 추가하고 오디오 큐와 같은 방법으로 설정했다. 이 점에서 중요한 것은 비디오와 오디오 스레드를 생성한 것이다. These bits do it:
SDL_PauseAudio(0);
break;
/* .... */
is->video_tid = SDL_CreateThread(video_thread, is);
지난 번 SDL_PauseAudio() 를 기억하며 SDL_CreateThread() 는 이전과 완전히 같은 방법으로 사용된다. 이제 video_thread() 함수로 돌아온다.
이전에 decode_thread() 함수의 두 번째 반쪽으로 돌아온다. 이 것은 기본적으로 루프이며 패킷을 읽고 적절한 큐에 전달하는 것이다.
for (;;) {
if (is->quit) {
break;
}
// seek stuff goes here
if (is->audioq.size > MAX_AUDIOQ_SIZE || is->videoq.size > MAX_VIDEOQ_SIZE) {
SDL_Delay(10);
continue;
}
if (av_read_frame(is->pFormatCtx, packet) < 0) {
if ((is->pFormatCtx->pb->error) == 0) {
SDL_Delay(100); /* 에러 아님; 사용자 입력 대기 */
continue;
} else {
break;
}
}
// 이 패킷이 비디오 스트림인가?
if (packet->stream_index == is->videoStream) {
packet_queue_put(&is->videoq, packet);
} else if (packet->stream_index == is->audioStream) {
packet_queue_put(&is->audioq, packet);
} else {
av_free_packet(packet);
}
}
그렇게 새로운 것은 없고 오디오와 비디오큐를 위한 최대 크기가 있으며 읽기 에러를 체크하는 부분을 추가했다. 포맷 컨텍스트는 pb라 불리는것의 내부에 ByteIOContext 구조체를 가진다. ByteIOContext는 하위 레벨 파일 정보를 담는 구조체이다.
루프 이후에 나머지 까지 기다리거나 끝낸것으로 알린다. 이 코드는 명령적인데 왜냐하면 이 것이 어떻게 이벤트를 넣는지 보여주기 때문이다. 비디오 표시할 어떤 것이다.
while (!is->quit) {
SDL_Delay(100);
}
fail:
if (1) {
SDL_Event event;
event.type = FF_QUIT_EVENT;
event.user.data1 = is;
SDL_PushEvent(&event);
}
return 0;
사용자 이벤트를 위한 값은 SDL 상수인 SDL_USEREVENT를 사용한다. 첫 사용자 이벤트는 SDL_USEREVENT 값으로 할당하고 다음은 SDL_USEREVENT+1 이다. FF_QUIT_EVENT는 프로그램에서 SDL_USEREVENT+1이다. 원한다면 사용자 데이터도 전달할 수 있고 여기서는 큰 구조체를 전달한다. 최종적으로 SDL_PushEvent() 를 호출한다. 이 것을 SDL_QUIT_EVENT 섹션으로 할당한다. 세부적으로 이벤트 루프를 살펴볼 것이다. 지금은, FF_QUIT_EVENT를 넣을 때 이를 감지하며 quit플래그를 설정할 것이라는 점이다.
프레임 얻기: video_thread
코덱이 준비되면 비디오 스레드를 시작한다. 이 스레드는 비디오 큐에서 패킷내부를 읽고 프레임으로 비디오를 디코드하고 픽쳐 큐에 처리된 프레임을 넣을 queue_picture 함수를 호출한다.
int video_thread(void* arg) {
VideoState* is = (VideoState*)arg;
AVPacket pkt1, *packet = &pkt1;
int frameFinished;
AVFrame* pFrame;
pFrame = av_frame_alloc();
for (;;) {
if (packet_queue_get(&is->videoq, packet, 1) < 0) {
// 패킷얻기를 종료한다
break;
}
// 비디오 프레임 디코드
avcodec_decode_video2(is->video_st->codec, pFrame, &frameFinished, packet);
// 비디오 프레임을 얻었는가?
if (frameFinished) {
if (queue_picture(is, pFrame) < 0) {
break;
}
}
av_free_packet(packet);
}
av_free(pFrame);
return 0;
}
이 함수의 대부분이 익숙해야 한다. avcodec_decode_video2 함수를 이곳에 이동시켰고 몇몇 매개변수룰 바꿨다 예를 들어 큰 구조체내의 AVStream을 저장하므로 코덱은 거기에서 얻는다. 비디오 큐로부터 패킷 얻기는 종료나 에러발생전까지는 계속한다.
프레임 큐잉
픽쳐 큐내의 pFrame인 디코드된 프레임을 저장하는 함수를 살펴보자. 우리의 픽쳐 큐가 SDL오버레이이므로 (가능한한 작은 계산만으로 비디오 디스플레이 함수를 허용하는) 이 프레임을 컨버트해야한다. 픽쳐 큐내에 저장한 데이터는 우리가 만드는 구조체이다.
typedef struct VideoPicture {
SDL_Overlay* bmp;
int width, height; /* source height & width */
int allocated;
} VideoPicture;
우리의 큰 구조체는 버퍼를 가져 이 곳에 저장할 수 있다. 그러나, SDL_Overlay 를 할당해야 한다. (allocated 플래그는 이를 했는지를 나타낸다.)
이 큐를 사용하려고 두개의 포인터를 가지는데 읽기 인덱스와 쓰기 인덱스이다. 또한 버퍼에 얼마나 많은 픽쳐가 유지되고 있는지 추적한다. 큐에 쓰기위해 먼저 버퍼를 대기하여 클리어하고 VideoPicture 를 저장하기위한 공간을 갖는다. 그러면 쓰기 인덱스에 오버레이를 할당했는지 확인한다. 만약 그렇지 않으면 할당한다. 윈도우의 크기가 변경되면 버퍼를 재할당해야 한다.
int queue_picture(VideoState* is, AVFrame* pFrame) {
VideoPicture* vp;
int dst_pix_fmt;
AVPicture pict;
/* 새로운 픽을 위한 공간까지 대기 */
SDL_LockMutex(is->pictq_mutex);
while (is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE && !is->quit) {
SDL_CondWait(is->pictq_cond, is->pictq_mutex);
}
SDL_UnlockMutex(is->pictq_mutex);
if (is->quit)
return -1;
// windex 는 초기에 0으로 설정한다
vp = &is->pictq[is->pictq_windex];
/* 버퍼를 할당하거나 리사이즈한다 */
if (!vp->bmp || vp->width != is->video_st->codec->width || vp->height != is->video_st->codec->height) {
SDL_Event event;
vp->allocated = 0;
alloc_picture(is);
if (is->quit) {
return -1;
}
}
alloc_picture() 함수를 살펴보자
void alloc_picture(void* userdata) {
VideoState* is = (VideoState*)userdata;
VideoPicture* vp;
vp = &is->pictq[is->pictq_windex];
if (vp->bmp) {
// 하나를 가지고 있으면 다른 것 하나 더 만든다, 큰 것/작은 것
SDL_FreeYUVOverlay(vp->bmp);
}
// 스크린상의 YUV이미지를 위치할 공간을 할당
SDL_LockMutex(screen_mutex);
vp->bmp = SDL_CreateYUVOverlay(is->video_st->codec->width, is->video_st->codec->height, SDL_YV12_OVERLAY, screen);
SDL_UnlockMutex(screen_mutex);
vp->width = is->video_st->codec->width;
vp->height = is->video_st->codec->height;
vp->allocated = 1;
}
SDL_CreateYUVOverlay 함수를 확인하자 메인 루프에서 이곳으로 옮겼다. 이 코드는 자체적으로 설명할 수 있을 것이다. 그러나, 뮤텍스 락을 주위에 했는데 두 스레드가 동시에 스크린에 대한 정보를 쓰지 못하게 하기 위함이다. 이는 함수가 픽쳐를 표시하는데 alloc_picture 함수가 엇갈려 호출되는 것을 방지하기 위한 것이다. (이 락은 전역변수에 할당했으며 메인함수에 초기화했다.) VideoPicture 구조체에는 너비와 높이를 저장하는데 왜냐하면 우리의 비디오 사이즈는 몇가지 이유에서 변경하지 않아야 하기 때문이다.
자, 준비되고 YUV 오버레이가 할당되었으며 이제 픽쳐를 받을 준비가 되었다. queue_picture 함수로 돌아가 프레임을 오버레이에 복사하는 부분을 살펴보자. 이 부분을 알 수 있을 것이다.
int queue_picture(VideoState* is, AVFrame* pFrame) {
/* 필요하다면 프레임을 할당 */
/* ... */
/* 픽쳐를 넣을 공간을 큐에 갖는다 */
if (vp->bmp) {
SDL_LockYUVOverlay(vp->bmp);
dst_pix_fmt = PIX_FMT_YUV420P;
/* 큐의 픽트를 가리킨다 */
pict.data[0] = vp->bmp->pixels[0];
pict.data[1] = vp->bmp->pixels[2];
pict.data[2] = vp->bmp->pixels[1];
pict.linesize[0] = vp->bmp->pitches[0];
pict.linesize[1] = vp->bmp->pitches[2];
pict.linesize[2] = vp->bmp->pitches[1];
// 이미지를 SDL이 사용하는 YUV 형식으로 컨버트
sws_scale(is->sws_ctx, (uint8_t const* const*)pFrame->data, pFrame->linesize, 0, is->video_st->codec->height, pict.data, pict.linesize);
SDL_UnlockYUVOverlay(vp->bmp);
/* 이제 디스플레이 스레드에 픽이 준비되었음을 알린다 */
if (++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
is->pictq_windex = 0;
}
SDL_LockMutex(is->pictq_mutex);
is->pictq_size++;
SDL_UnlockMutex(is->pictq_mutex);
}
return 0;
}
이 부분의 주요부분은 프레임으로 YUV오버레이를 채우는 코드이다. 마지막 비트는 단순히 큐에 우리 값을 추가하는 것이다. 큐는 이게 가득 찰 때까지 작업을 계속하고 여기에 있기만 하면 읽어들인다. 그러므로 모든 것은 is_pictq_size에 달려 있으니 이 것을 락해야할 필요가 생긴다. 그래서 여기서 하는 것은 저장 포인터를 증가시키고 (필요하다면 롤오버) 큐를 락하고 사이즈를 증가시킨다. 이제 큐에 더많은 정보가 있음을 알며 이 것이 큐를 가득차게 하면 이에 대해 알 수 있다.
비디오 표시하기
비디오 스레드가 끝났다! 이제 하나만 제외하고 모든 스레드를 살펴봤다. 이전에 schedule_refresh()함수를 호출했을 것이다. 이게 무엇을 하는지 살펴보자.
/* 비디오 새로고침을 주어진 '딜레이' 마이크로 세컨드에 스케줄 한다 */
static void schedule_refresh(VideoState* is, int delay) {
SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}
SDL_AddTimer() 는 SDL함수로서 특정 밀리세컨드 이후에 사용자가 지정한 함수를 호출하는 콜백을 만든다. (선택적으로 몇몇 사용자 데이터도 전달한다) 이 함수를 사용해 비디오 업데이트를 스케줄한다. 매번 이 함수를 호출한다. 타이머를 설정하고 이벤트를 트리거하며 매인 함수에서는 픽쳐 큐에서 프레임을 당기고 표시한다.
먼저 이벤트를 트리거한다. 이는 다음과 같다.
static Uint32 sdl_refresh_timer_cb(Uint32 interval, void* opaque) {
SDL_Event event;
event.type = FF_REFRESH_EVENT;
event.user.data1 = opaque;
SDL_PushEvent(&event);
return 0; /* 0 은 타이머 멈춤을 의미한다 */
}
이제 익숙한 이벤트 푸시가 있다. FF_REFRESH_EVENT는 SDL_USEREVENT+1로 정의된다. 중요한 것은 return 0할 때 SDL이 타이머를 멈춰서 콜백이 다시 만들어지지 않는 점이다.
이제 FF_REFRESH_EVENT를 푸시하면 이벤트 루프에서 이를 처리해야 한다.
for (;;) {
SDL_WaitEvent(&event);
switch (event.type) {
/* ... */
case FF_REFRESH_EVENT:
video_refresh_timer(event.user.data1);
break;
그리고 이 함수로 보내며 여기서 픽쳐 큐로부터 실제로 데이터를 당긴다.
void video_refresh_timer(void* userdata) {
VideoState* is = (VideoState*)userdata;
VideoPicture* vp;
if (is->video_st) {
if (is->pictq_size == 0) {
schedule_refresh(is, 1);
} else {
vp = &is->pictq[is->pictq_rindex];
/* 타이밍 코드는 여기에 온다 */
schedule_refresh(is, 80);
/* 픽쳐를 보여준다! */
video_display(is);
/* 다음 픽쳐를 위해 큐를 업데이트한다 */
if (++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
is->pictq_rindex = 0;
}
SDL_LockMutex(is->pictq_mutex);
is->pictq_size--;
SDL_CondSignal(is->pictq_cond);
SDL_UnlockMutex(is->pictq_mutex);
}
} else {
schedule_refresh(is, 100);
}
}
있으면 큐에서 당겨오고 다음 비디오 프레임이 보여져야할 때를 지정한 타이머를 설정한다. video_display를 호출해 스크린에 비디오를 보이게 하고 큐의 카운터를 증가시키고 그 사이즈는 감소시킨다. 이 함수에서는 vp에 대한 어떤 것도 하지 않음을 유념하자. 오디오에 비디오를 싱크할 때 타이밍 정보를 사용한다. "타이밍이 여기에 있어야 하나?" 가 있는 부분을 살펴보자. 그 섹션에서, 우리가 어떻게 다음 비디오 프레임을 보여야하는지 살펴보고 그 값을 schedule_refresh() 함수에 넣는다. 일단 더미값인 80을 할당한다. 기술적으로, 이 값을 예측하고 확인하며 영상을 봄으로서 재컴파일한다. 어쨌든 다시 돌아올 것이다.
이제 거의 마쳤다. 남은 하나는 비디오 보여주기이다. video_display함수
void video_display(VideoState* is) {
SDL_Rect rect;
VideoPicture* vp;
float aspect_ratio;
int w, h, x, y;
int i;
vp = &is->pictq[is->pictq_rindex];
if (vp->bmp) {
if (is->video_st->codec->sample_aspect_ratio.num == 0) {
aspect_ratio = 0;
} else {
aspect_ratio = av_q2d(is->video_st->codec->sample_aspect_ratio) * is->video_st->codec->width / is->video_st->codec->height;
}
if (aspect_ratio <= 0.0) {
aspect_ratio = (float)is->video_st->codec->width / (float)is->video_st->codec->height;
}
h = screen->h;
w = ((int)rint(h * aspect_ratio)) & -3;
if (w > screen->w) {
w = screen->w;
h = ((int)rint(w / aspect_ratio)) & -3;
}
x = (screen->w - w) / 2;
x = (screen->h - h) / 2;
rect.x = x;
rect.y = y;
rect.w = w;
rect.h = h;
SDL_LockMutex(screen_mutex);
SDL_DisplayYUVOverlay(vp->bmp, &rect);
SDL_UnlockMutex(screen_mutex);
}
}
스크린이 어떤 사이즈이든 될 수있으므로 (640x480으로 설정하더라도 사용자에의해 리사이즈될 수 있다.) 동적으로 영화의 사각형이 어떻게 되는지 살펴야 한다. 먼저 영화의 비율로서 너비를 높이로 나눈값이다. 몇몇 코덱은 특이한 샘플 비율을 가지는데 단일 픽셀이나 샘플의 너비/높이 비율이다. 코덱컨텍스트에서 높이와 너비는 픽셀로 측정되므로 실제 비율은 샘플 비율배이다. 몇몇 코덱은 0의 비율이고 이는 각 픽셀이 1x1임을 나타낸다. 그러면 스크린에 최대한 크게 보여져야 한다. & -3 은 4의 배수의 가장가까운 값으로 반올림하는 비트 트위들링이다. 그러면 영상의 중심을 잡고 SDL_DisplayYUVOverlay() 를 호출하여 스크린 뮤텍스를 사용해 여기에 접근하도록 한다.
여기까지인가? 마쳤는가? 음, 우리는 오디오 코덱을 다시작성해 새로운 VideoStruct 를 사용하도록 해야 하지만 간단한 변화이므로 샘플코드를 살펴보자. 마지막으로 해야할 것은 FFmpeg의 내부 quit 콜백 함수를 호출하도록 변경하는 것이다.
VideoState* global_video_state;
int decode_interrupt_cb(void) {
return (global_video_state && global_video_state->quit);
}
global_video_state를 설정은 메인에서 수행한다.
여기까지다! 컴파일한다.
gcc -o tutorial04 tutorial04.c -lavutil -lavformat -lswscale -lz -lm `sdl-config --cflags --libs`
언싱크된 영상이 보여진다. 다음 번에는 실제로 작동하는 비디오 재생기를 빌드한다.
// tutorial04.c
// A pedagogical video player that will stream through every video frame as fast as it can,
// and play audio (out of sync).
//
// Code based on FFplay, Copyright (c) 2003 Fabrice Bellard,
// and a tutorial by Martin Bohme (boehme@inb.uni-luebeckREMOVETHIS.de)
// Tested on Gentoo, CVS version 5/01/07 compiled with GCC 4.1.1
// With updates from https://github.com/chelyaev/ffmpeg-tutorial
// Updates tested on:
// LAVC 54.59.100, LAVF 54.29.104, LSWS 2.1.101, SDL 1.2.15
// on GCC 4.7.2 in Debian February 2015
// Use
//
// gcc -o tutorial04 tutorial04.c -lavformat -lavcodec -lswscale -lz -lm `sdl-config --cflags --libs`
// to build (assuming libavformat and libavcodec are correctly installed,
// and assuming you have sdl-config. Please refer to SDL docs for your installation.)
//
// Run using
// tutorial04 myvideofile.mpg
//
// to play the video stream on your screen.
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <SDL.h>
#include <SDL_thread.h>
#ifdef __MINGW32__
#undef main /* Prevents SDL from overriding main() */
#endif
#include <stdio.h>
#include <assert.h>
#include <math.h>
// compatibility with newer API
#if LIBAVCODEC_VERSION_INT < AV_VERSION_INT(55,28,1)
#define av_frame_alloc avcodec_alloc_frame
#define av_frame_free avcodec_free_frame
#endif
#define SDL_AUDIO_BUFFER_SIZE 1024
#define MAX_AUDIO_FRAME_SIZE 192000
#define MAX_AUDIOQ_SIZE (5 * 16 * 1024)
#define MAX_VIDEOQ_SIZE (5 * 256 * 1024)
#define FF_REFRESH_EVENT (SDL_USEREVENT)
#define FF_QUIT_EVENT (SDL_USEREVENT + 1)
#define VIDEO_PICTURE_QUEUE_SIZE 1
typedef struct PacketQueue {
AVPacketList *first_pkt, *last_pkt;
int nb_packets;
int size;
SDL_mutex *mutex;
SDL_cond *cond;
} PacketQueue;
typedef struct VideoPicture {
SDL_Overlay *bmp;
int width, height; /* source height & width */
int allocated;
} VideoPicture;
typedef struct VideoState {
AVFormatContext *pFormatCtx;
int videoStream, audioStream;
AVStream *audio_st;
AVCodecContext *audio_ctx;
PacketQueue audioq;
uint8_t audio_buf[(AVCODEC_MAX_AUDIO_FRAME_SIZE * 3) / 2];
unsigned int audio_buf_size;
unsigned int audio_buf_index;
AVFrame audio_frame;
AVPacket audio_pkt;
uint8_t *audio_pkt_data;
int audio_pkt_size;
AVStream *video_st;
AVCodecContext *video_ctx;
PacketQueue videoq;
struct SwsContext *sws_ctx;
VideoPicture pictq[VIDEO_PICTURE_QUEUE_SIZE];
int pictq_size, pictq_rindex, pictq_windex;
SDL_mutex *pictq_mutex;
SDL_cond *pictq_cond;
SDL_Thread *parse_tid;
SDL_Thread *video_tid;
char filename[1024];
int quit;
} VideoState;
SDL_Surface *screen;
SDL_mutex *screen_mutex;
/* Since we only have one decoding thread, the Big Struct
can be global in case we need it. */
VideoState *global_video_state;
void packet_queue_init(PacketQueue *q) {
memset(q, 0, sizeof(PacketQueue));
q->mutex = SDL_CreateMutex();
q->cond = SDL_CreateCond();
}
int packet_queue_put(PacketQueue *q, AVPacket *pkt) {
AVPacketList *pkt1;
if(av_dup_packet(pkt) < 0) {
return -1;
}
pkt1 = av_malloc(sizeof(AVPacketList));
if (!pkt1)
return -1;
pkt1->pkt = *pkt;
pkt1->next = NULL;
SDL_LockMutex(q->mutex);
if (!q->last_pkt)
q->first_pkt = pkt1;
else
q->last_pkt->next = pkt1;
q->last_pkt = pkt1;
q->nb_packets++;
q->size += pkt1->pkt.size;
SDL_CondSignal(q->cond);
SDL_UnlockMutex(q->mutex);
return 0;
}
static int packet_queue_get(PacketQueue *q, AVPacket *pkt, int block)
{
AVPacketList *pkt1;
int ret;
SDL_LockMutex(q->mutex);
for(;;) {
if(global_video_state->quit) {
ret = -1;
break;
}
pkt1 = q->first_pkt;
if (pkt1) {
q->first_pkt = pkt1->next;
if (!q->first_pkt)
q->last_pkt = NULL;
q->nb_packets--;
q->size -= pkt1->pkt.size;
*pkt = pkt1->pkt;
av_free(pkt1);
ret = 1;
break;
} else if (!block) {
ret = 0;
break;
} else {
SDL_CondWait(q->cond, q->mutex);
}
}
SDL_UnlockMutex(q->mutex);
return ret;
}
int audio_decode_frame(VideoState *is, uint8_t *audio_buf, int buf_size) {
int len1, data_size = 0;
AVPacket *pkt = &is->audio_pkt;
for(;;) {
while(is->audio_pkt_size > 0) {
int got_frame = 0;
len1 = avcodec_decode_audio4(is->audio_ctx, &is->audio_frame, &got_frame, pkt);
if(len1 < 0) {
/* if error, skip frame */
is->audio_pkt_size = 0;
break;
}
data_size = 0;
if(got_frame) {
data_size = av_samples_get_buffer_size(NULL,
is->audio_ctx->channels,
is->audio_frame.nb_samples,
is->audio_ctx->sample_fmt,
1);
assert(data_size <= buf_size);
memcpy(audio_buf, is->audio_frame.data[0], data_size);
}
is->audio_pkt_data += len1;
is->audio_pkt_size -= len1;
if(data_size <= 0) {
/* No data yet, get more frames */
continue;
}
/* We have data, return it and come back for more later */
return data_size;
}
if(pkt->data)
av_free_packet(pkt);
if(is->quit) {
return -1;
}
/* next packet */
if(packet_queue_get(&is->audioq, pkt, 1) < 0) {
return -1;
}
is->audio_pkt_data = pkt->data;
is->audio_pkt_size = pkt->size;
}
}
void audio_callback(void *userdata, Uint8 *stream, int len) {
VideoState *is = (VideoState *)userdata;
int len1, audio_size;
while(len > 0) {
if(is->audio_buf_index >= is->audio_buf_size) {
/* We have already sent all our data; get more */
audio_size = audio_decode_frame(is, is->audio_buf, sizeof(is->audio_buf));
if(audio_size < 0) {
/* If error, output silence */
is->audio_buf_size = 1024;
memset(is->audio_buf, 0, is->audio_buf_size);
} else {
is->audio_buf_size = audio_size;
}
is->audio_buf_index = 0;
}
len1 = is->audio_buf_size - is->audio_buf_index;
if(len1 > len)
len1 = len;
memcpy(stream, (uint8_t *)is->audio_buf + is->audio_buf_index, len1);
len -= len1;
stream += len1;
is->audio_buf_index += len1;
}
}
static Uint32 sdl_refresh_timer_cb(Uint32 interval, void *opaque) {
SDL_Event event;
event.type = FF_REFRESH_EVENT;
event.user.data1 = opaque;
SDL_PushEvent(&event);
return 0; /* 0 means stop timer */
}
/* schedule a video refresh in 'delay' ms */
static void schedule_refresh(VideoState *is, int delay) {
SDL_AddTimer(delay, sdl_refresh_timer_cb, is);
}
void video_display(VideoState *is) {
SDL_Rect rect;
VideoPicture *vp;
float aspect_ratio;
int w, h, x, y;
int i;
vp = &is->pictq[is->pictq_rindex];
if(vp->bmp) {
if(is->video_ctx->sample_aspect_ratio.num == 0) {
aspect_ratio = 0;
} else {
aspect_ratio = av_q2d(is->video_ctx->sample_aspect_ratio) *
is->video_ctx->width / is->video_ctx->height;
}
if(aspect_ratio <= 0.0) {
aspect_ratio = (float)is->video_ctx->width /
(float)is->video_ctx->height;
}
h = screen->h;
w = ((int)rint(h * aspect_ratio)) & -3;
if(w > screen->w) {
w = screen->w;
h = ((int)rint(w / aspect_ratio)) & -3;
}
x = (screen->w - w) / 2;
y = (screen->h - h) / 2;
rect.x = x;
rect.y = y;
rect.w = w;
rect.h = h;
SDL_LockMutex(screen_mutex);
SDL_DisplayYUVOverlay(vp->bmp, &rect);
SDL_UnlockMutex(screen_mutex);
}
}
void video_refresh_timer(void *userdata) {
VideoState *is = (VideoState *)userdata;
VideoPicture *vp;
if(is->video_st) {
if(is->pictq_size == 0) {
schedule_refresh(is, 1);
} else {
vp = &is->pictq[is->pictq_rindex];
/* Now, normally here goes a ton of code
about timing, etc. we're just going to
guess at a delay for now. You can
increase and decrease this value and hard code
the timing - but I don't suggest that ;)
We'll learn how to do it for real later.
*/
schedule_refresh(is, 40);
/* show the picture! */
video_display(is);
/* update queue for next picture! */
if(++is->pictq_rindex == VIDEO_PICTURE_QUEUE_SIZE) {
is->pictq_rindex = 0;
}
SDL_LockMutex(is->pictq_mutex);
is->pictq_size--;
SDL_CondSignal(is->pictq_cond);
SDL_UnlockMutex(is->pictq_mutex);
}
} else {
schedule_refresh(is, 100);
}
}
void alloc_picture(void *userdata) {
VideoState *is = (VideoState *)userdata;
VideoPicture *vp;
vp = &is->pictq[is->pictq_windex];
if(vp->bmp) {
// we already have one make another, bigger/smaller
SDL_FreeYUVOverlay(vp->bmp);
}
// Allocate a place to put our YUV image on that screen
SDL_LockMutex(screen_mutex);
vp->bmp = SDL_CreateYUVOverlay(is->video_ctx->width,
is->video_ctx->height,
SDL_YV12_OVERLAY,
screen);
SDL_UnlockMutex(screen_mutex);
vp->width = is->video_ctx->width;
vp->height = is->video_ctx->height;
vp->allocated = 1;
}
int queue_picture(VideoState *is, AVFrame *pFrame) {
VideoPicture *vp;
int dst_pix_fmt;
AVPicture pict;
/* wait until we have space for a new pic */
SDL_LockMutex(is->pictq_mutex);
while(is->pictq_size >= VIDEO_PICTURE_QUEUE_SIZE &&
!is->quit) {
SDL_CondWait(is->pictq_cond, is->pictq_mutex);
}
SDL_UnlockMutex(is->pictq_mutex);
if(is->quit)
return -1;
// windex is set to 0 initially
vp = &is->pictq[is->pictq_windex];
/* allocate or resize the buffer! */
if(!vp->bmp ||
vp->width != is->video_ctx->width ||
vp->height != is->video_ctx->height) {
SDL_Event event;
vp->allocated = 0;
alloc_picture(is);
if(is->quit) {
return -1;
}
}
/* We have a place to put our picture on the queue */
if(vp->bmp) {
SDL_LockYUVOverlay(vp->bmp);
dst_pix_fmt = PIX_FMT_YUV420P;
/* point pict at the queue */
pict.data[0] = vp->bmp->pixels[0];
pict.data[1] = vp->bmp->pixels[2];
pict.data[2] = vp->bmp->pixels[1];
pict.linesize[0] = vp->bmp->pitches[0];
pict.linesize[1] = vp->bmp->pitches[2];
pict.linesize[2] = vp->bmp->pitches[1];
// Convert the image into YUV format that SDL uses
sws_scale(is->sws_ctx, (uint8_t const * const *)pFrame->data,
pFrame->linesize, 0, is->video_ctx->height,
pict.data, pict.linesize);
SDL_UnlockYUVOverlay(vp->bmp);
/* now we inform our display thread that we have a pic ready */
if(++is->pictq_windex == VIDEO_PICTURE_QUEUE_SIZE) {
is->pictq_windex = 0;
}
SDL_LockMutex(is->pictq_mutex);
is->pictq_size++;
SDL_UnlockMutex(is->pictq_mutex);
}
return 0;
}
int video_thread(void *arg) {
VideoState *is = (VideoState *)arg;
AVPacket pkt1, *packet = &pkt1;
int frameFinished;
AVFrame *pFrame;
pFrame = av_frame_alloc();
for(;;) {
if(packet_queue_get(&is->videoq, packet, 1) < 0) {
// means we quit getting packets
break;
}
// Decode video frame
avcodec_decode_video2(is->video_ctx, pFrame, &frameFinished, packet);
// Did we get a video frame?
if(frameFinished) {
if(queue_picture(is, pFrame) < 0) {
break;
}
}
av_free_packet(packet);
}
av_frame_free(&pFrame);
return 0;
}
int stream_component_open(VideoState *is, int stream_index) {
AVFormatContext *pFormatCtx = is->pFormatCtx;
AVCodecContext *codecCtx = NULL;
AVCodec *codec = NULL;
SDL_AudioSpec wanted_spec, spec;
if(stream_index < 0 || stream_index >= pFormatCtx->nb_streams) {
return -1;
}
codec = avcodec_find_decoder(pFormatCtx->streams[stream_index]->codec->codec_id);
if(!codec) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
codecCtx = avcodec_alloc_context3(codec);
if(avcodec_copy_context(codecCtx, pFormatCtx->streams[stream_index]->codec) != 0) {
fprintf(stderr, "Couldn't copy codec context");
return -1; // Error copying codec context
}
if(codecCtx->codec_type == AVMEDIA_TYPE_AUDIO) {
// Set audio settings from codec info
wanted_spec.freq = codecCtx->sample_rate;
wanted_spec.format = AUDIO_S16SYS;
wanted_spec.channels = codecCtx->channels;
wanted_spec.silence = 0;
wanted_spec.samples = SDL_AUDIO_BUFFER_SIZE;
wanted_spec.callback = audio_callback;
wanted_spec.userdata = is;
if(SDL_OpenAudio(&wanted_spec, &spec) < 0) {
fprintf(stderr, "SDL_OpenAudio: %s\n", SDL_GetError());
return -1;
}
}
if(avcodec_open2(codecCtx, codec, NULL) < 0) {
fprintf(stderr, "Unsupported codec!\n");
return -1;
}
switch(codecCtx->codec_type) {
case AVMEDIA_TYPE_AUDIO:
is->audioStream = stream_index;
is->audio_st = pFormatCtx->streams[stream_index];
is->audio_ctx = codecCtx;
is->audio_buf_size = 0;
is->audio_buf_index = 0;
memset(&is->audio_pkt, 0, sizeof(is->audio_pkt));
packet_queue_init(&is->audioq);
SDL_PauseAudio(0);
break;
case AVMEDIA_TYPE_VIDEO:
is->videoStream = stream_index;
is->video_st = pFormatCtx->streams[stream_index];
is->video_ctx = codecCtx;
packet_queue_init(&is->videoq);
is->video_tid = SDL_CreateThread(video_thread, is);
is->sws_ctx = sws_getContext(is->video_ctx->width, is->video_ctx->height,
is->video_ctx->pix_fmt, is->video_ctx->width,
is->video_ctx->height, PIX_FMT_YUV420P,
SWS_BILINEAR, NULL, NULL, NULL
);
break;
default:
break;
}
}
int decode_thread(void *arg) {
VideoState *is = (VideoState *)arg;
AVFormatContext *pFormatCtx;
AVPacket pkt1, *packet = &pkt1;
int video_index = -1;
int audio_index = -1;
int i;
is->videoStream=-1;
is->audioStream=-1;
global_video_state = is;
// Open video file
if(avformat_open_input(&pFormatCtx, is->filename, NULL, NULL)!=0)
return -1; // Couldn't open file
is->pFormatCtx = pFormatCtx;
// Retrieve stream information
if(avformat_find_stream_info(pFormatCtx, NULL)<0)
return -1; // Couldn't find stream information
// Dump information about file onto standard error
av_dump_format(pFormatCtx, 0, is->filename, 0);
// Find the first video stream
for(i=0; i<pFormatCtx->nb_streams; i++) {
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO &&
video_index < 0) {
video_index=i;
}
if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_AUDIO &&
audio_index < 0) {
audio_index=i;
}
}
if(audio_index >= 0) {
stream_component_open(is, audio_index);
}
if(video_index >= 0) {
stream_component_open(is, video_index);
}
if(is->videoStream < 0 || is->audioStream < 0) {
fprintf(stderr, "%s: could not open codecs\n", is->filename);
goto fail;
}
// main decode loop
for(;;) {
if(is->quit) {
break;
}
// seek stuff goes here
if(is->audioq.size > MAX_AUDIOQ_SIZE ||
is->videoq.size > MAX_VIDEOQ_SIZE) {
SDL_Delay(10);
continue;
}
if(av_read_frame(is->pFormatCtx, packet) < 0) {
if(is->pFormatCtx->pb->error == 0) {
SDL_Delay(100); /* no error; wait for user input */
continue;
} else {
break;
}
}
// Is this a packet from the video stream?
if(packet->stream_index == is->videoStream) {
packet_queue_put(&is->videoq, packet);
} else if(packet->stream_index == is->audioStream) {
packet_queue_put(&is->audioq, packet);
} else {
av_free_packet(packet);
}
}
/* all done - wait for it */
while(!is->quit) {
SDL_Delay(100);
}
fail:
if(1){
SDL_Event event;
event.type = FF_QUIT_EVENT;
event.user.data1 = is;
SDL_PushEvent(&event);
}
return 0;
}
int main(int argc, char *argv[]) {
SDL_Event event;
VideoState *is;
is = av_mallocz(sizeof(VideoState));
if(argc < 2) {
fprintf(stderr, "Usage: test <file>\n");
exit(1);
}
// Register all formats and codecs
av_register_all();
if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)) {
fprintf(stderr, "Could not initialize SDL - %s\n", SDL_GetError());
exit(1);
}
// Make a screen to put our video
#ifndef __DARWIN__
screen = SDL_SetVideoMode(640, 480, 0, 0);
#else
screen = SDL_SetVideoMode(640, 480, 24, 0);
#endif
if(!screen) {
fprintf(stderr, "SDL: could not set video mode - exiting\n");
exit(1);
}
screen_mutex = SDL_CreateMutex();
av_strlcpy(is->filename, argv[1], sizeof(is->filename));
is->pictq_mutex = SDL_CreateMutex();
is->pictq_cond = SDL_CreateCond();
schedule_refresh(is, 40);
is->parse_tid = SDL_CreateThread(decode_thread, is);
if(!is->parse_tid) {
av_free(is);
return -1;
}
for(;;) {
SDL_WaitEvent(&event);
switch(event.type) {
case FF_QUIT_EVENT:
case SDL_QUIT:
is->quit = 1;
SDL_Quit();
return 0;
break;
case FF_REFRESH_EVENT:
video_refresh_timer(event.user.data1);
break;
default:
break;
}
}
return 0;
}
반응형
'FFmpeg' 카테고리의 다른 글
FFmpeg and SDL Tutorial - Synching Audio (1) | 2023.03.19 |
---|---|
FFmpeg and SDL Tutorial - Synching Video (0) | 2023.03.19 |
FFmpeg and SDL Tutorial - Playing Sound (0) | 2023.03.19 |
FFmpeg and SDL Tutorial - Outputting to the Screen (0) | 2023.03.19 |
FFmpeg and SDL Tutorial - Making Screencaps (0) | 2023.03.19 |