Live555에 보면 RTSP스트림을 받을 수 있는 소스코드를 이미 제공하고 있다.

하나는 openRTSP.c이고 다른하나는 testRTSPClinet.c라는 소스코드이다.

 

openRTSP는 RTSP규약을 전부다 구현해 놓은 Client 프로그램으로 재생 정지 되감기 등의 RTSP기능을 가지고 있다. 즉, 좀 보기 복잡하다.. ㅎㅎ 반면에 testRTSPClient는 RTSP스트림을 받는 것만 구현이 되어 있어서 기능은 그닥 뭐 볼건 없지만, Live555가 제공하고 있는 파이프라인 구조라든지 RTSP client와 server간의 동작을 간단히 살펴 보는데는 더할 나위 없이 좋다. 

 

무엇보다도 내가 만들고자 했던 프로그램은 항상 Live영상만 H.264로 전달하는 IP Camera의 RTSP서비스를 활용하기 때문에 다양한 RTSP규약의 기능을 별로 알필요가 없어 구지 어려운 소스코드를 볼 필요가 없었다. =_=

 

어쨋든 이번 포스트에서는 testRTSPClient를 대상으로 해서 어떻게 RTSP서비스로부터 데이터를 수신할 것인지, 그리고 어떻게 파이프라인을 구성해서 활용하고 있는지를 살펴볼 것이다.

 

먼저 main부터 살펴보자

 

이미지로 찍어 올렸더니 잘 안보인다.

소스코드는 testRTSPClient.c에 들어가 있기 때문에 그 소스코드를 열어 보시면 좋겠다.

어찌됐든 위 부분이 console타입의 testRTSPClient프로그램의 도입부인 main함수이다.

 

생각외로 매우 조촐하다.

보이는 함수도 별로 없고 심지어는 지역변수도  별로 없다.

 

Main함수가 이토록 조촐한 이유는 Live555가 MFC의 메시지 드리븐 방식과 비슷하게 동작하기 때문이다.

(메시지 드리븐이라는 말은 큐에 메시지가 들어가고, 각 메시지가 발생한 순서대로 처리되는 방식을 의미한다.)

 

Live555에서는 이런 메시지 드리븐 방식의 동작구조를 TaskScheduler가 제공하고 있으며, main초반부에 보면 TaskScheduler를 생성하여 사용하고 잇는 것을 볼 수 있다. 아울러 그 다음에 생성된 UsageEnvironment는 이전 포스트에서 설명 했듯이, OS와 Live555의 기능 사이에서 소켓등 OS에 종속된 기능들을 추상화 하여 Live555에 제공하는 역할을 한다. 이 클래스 때문에 Live555함수의 기능들은 OS 따라 함수코드를 바꿀 필요 없이, UsageEnvironment만 바라보고 소스코드의 일관성을 유지 할 수 있다. 실제로 OS에 따라 바뀌는 것은 UsageEnvironment이다.

(OS에 따라 UsageEnvironment를 상속하여 적합한 UsageEnvironment를 제공해야 하며 Windows, Linux에서는 BasicUsageEnvironment를 사용하면 된다.) 

 

어쨋든 main에서는 이제 프로그램의 아규먼트로 입력받은 RTSP URL를 인자로 하여 openURL함수를 호출한다.

그리고서는 메시지 큐가 정상적으로 동작을 시작하도록 taskScheduler의 doEventLoop를 실행한다.

이 doEventLoop는 글로벌 변수로 어떤 변수를 받는데, 이 글로벌 변수가 0이 아닌 값이 들어가면, 이벤트 루프를 종료하고 return 0; 문으로 넘어가 프로그램이 종료되게 한다.

 

그럼 이제 RTSP URL로 뭘하는지 실제 OpenURL 함수의 구조를 살펴보자.

openURL함수는 아래와 같다.

 

역시 이미지로 올렸더니 흐릿하니 잘 안보인다. testRTSPClient.c를 확인하시라.. =_=

 

openURL에서는 ourRTSPClient를 하나 생성해서 해당 URL로 대표되는 RTSP서비스를 처리하도록 하고 있다.

그 다음에 sendDescribeCommand를 이용해 RTSP서버로 DESCRIBE명령을 전송한다. sendDescribeCommand의 인자로 주어진 continueAfterDESCRIBE는 콜백함수 포인터로, 서버로 보낸 DESCRIBE명령에 대한 응답이 돌아왔을 때 호출될 콜백함수의 포인터이다.

 

RTSP서비스를 제공하는 RTSP서버에 DESCRIBE명령을 전송하면 RTSP서버는 클라이언트로 SDP ( Simple Description Protocol ) 문서를 전달한다. 문서라고 하니까 무슨 파일로 되어 있는 그런걸 떠올릴 수 있는데 파일은 아니고 단순히 어떤 문자열에 불과하다. 이 SDP문자열에는 해당 RTSP서비스가 제공하고 있는 미디어스트림의 종류 등 정보가 들어가 있다.

 

예를 들자면.. 아래와 같은 SDP는 오디오, 비디오 2종류의 미디어를 제공하고 있다는 의미이다..

 

m=audio 49170 RTP/AVP 0 8 97

a=rtpmap:0 PCMU/8000

a=rtpmap:8 PCMA/8000

a=rtpmap:97 iLBC/8000

m=video 51372 RTP/AVP 31

a=rtpmap:31 H261/90000

 

특히 "m=audio", audio미디어를 49170포트로 Realtime Transport Protocol / Audio Video Payloadtype

 에 정의된 0,8,97번 즉, PCMU/8000, PCMA/8000, iLBC/8000으로 제공하고

"m=video", video미디어는 51372포트로 Realtime Transport Protocol / Audio Video Payloadtype 에 정의된 31번

즉, H261/90000으로 제공한다는 의미를 가지고 있다는 의미이다.

 

물론 이런 정보를 알아내기 위해서 SDP를 공부할 필요는 없다.

Live555에서 SDP를 파싱해서 우리가 이해 가능한 정보로 바꿔주는 기능을 제공해 주니 두려워 않아도 좋다.

 

그럼 이제 continueAfterDESCRIBE함수에서 어떻게 이 SDP를 처리하고 있는지 살펴보자. 

허허... 그림이 커질수록 점점 더 안보이기 시작하고 있다..=_=

 

어쨋든 눈을 부릅뜨고 한번 보자. 참고로 이 continueAfterDESCRIBE는 static 함수로 어떤 rtspClient의 DESCRIBE명령로부터 콜백 된 함수인지 모르기 때문에 Live555에서는 함수의 인자로 rtspClient를 전달해준다. 그리고 3번째 인자로 resultString을 건네주는데 이게 SDP 스트링이다.

 

이걸 SDP를 파싱해서 쓰기 위해서 205줄과 같이 MediaSession을 만든다. 인자로 sdp문자열을 전달하는데 이렇게 되면 자동으로 SDP에 들어있는 미디어 스트림 정보마다 MediaSubsession이 생성되어 MediaSession에 추가된다.

 

그리고는 221 줄과 같이 각 MediaSubsession에 대해 setup을 시도한다. setup은 각 MediaSubsession이 제공하는 미디어 스트림데이터를 받을 수 있는 서버-클라이언트 간 환경을 조성하는 과정이다. setupNextSubsession( ) 함수는 rtspClient를 인수로 받아 이과정을 수행한다.

 

참고로 ourRTSPClinet를 살펴보면 아래와 같이 RTSP연결을 의미하는 MediaSession, RTSP연결에서 제공하는 미디어 스트림연결을 의미하는 MediaSubsession, 그리고 각 MediaSubsession을 순회할 수 있는 MediaSubSessionIterator정보가.. 정확히는 이런 정보를 가지고 있는 StreamClientState 인스턴스가 들어가 있다.

 

 

자세한 부분은 이 포스트를 전체적으로 한번 읽어보고 중단점을 걸어 한단계씩 넘어가다보면 어떻게 ourRTSPClient가 각 MediaSubSession등의 정보를 저장하고 있는지 알 수 있을 것이다.

 

자, 그럼 이제 setupNextSubsession에서 어떻게 서버 - 클라이언트간 미디어 스트림 데이터 수신 환경을 구축하고 있는지 살펴보자. 아래는 setupNextSubsession이다.

 

 

허허.. 쪼매 길다. 코드가

 

 

어쨋든 처음부터 차근차근 살펴보자..

다소 복잡해 보이지만, 전체 적인 코드 구조는 아래와 같다.

 

    1. 인자로 넘겨받은 rtspClient포인터를 전달받아 ourRTSPClient객체를 찾는다.

    2. ourRTSPClient객체의 StreamClietState멤버로부터 MediaSession이 가진 MediaSubsession을 순회

    3. 각 MediaSubSession에 대해 Initiate를 호출해 초기화한다.

    4. 각 MediaSubSession에 대해 sendSetupCommand명령을 전송한다.

    5. 모든 MediaSubSession에 대해 sendSetupCommand를 전송하고 순회가 완료 (NULL)

    6. sendPlayCommand로 Play Command를 RTSP서버로 전송한다.

 

이 setupNextSubsession함수는 모든 MediaSubsession을 순회하도록 재귀함수 구조로 되어 있는데, MeidaSubsession이 실패한 경우는 이 함수내에서 직접적으로 setupNextSubsession을 247줄과 같이 호출해서, 성공한 경우는 sendSetupCommand의 인자로 전달한 continueAfterSetup 콜백에서 재귀적으로 다시 호출된다.

Posted by 굿쟌
,

Live555는 데이터를 처리하기 위해 데이터 파이프라인 구조를 제공하고 있다.

말이 어렵지만 사실 별거 없다.

파이프라인을 제공한다는 말은 물이 파이프를 따라 흐르듯이 데이터도 어떤 순서에 따라 파이프라인의

각 부분을 통과하게 되고, 각 부분에서 필요한 처리가 이뤄진다는 의미이다.

 

아울러 물이 파이프라인의 한쪽 방향에서 다른 방향으로만 흐르듯이

Live555에서도 데이터는 항상 한쪽 방향으로만 흐른다.

 

Live555의 파이프라인 구조는

단순히 데이터를 생산해 내다음 단계로 넘기는 Source,

데이터를 다음 단계로 넘기기도 하지만 받기도 하는 Filter

데이터를 받기만 하는 Sink로

이뤄져 있으며

 

Source -> Filter -> Filter -> Sink 혹은

Source -> Filter -> Sink 또는

Source -> Sink 와 같이 구성 할 수 있다.

 

Filter는 데이터를 전달하는 특성이 Source와 동일하기 때문에 클래스 구조상 Filter는 Source를 상속받은

클래스 이고 따라서 Filter에 대한 참조를 Source클래스의 포인터로 해도 무방하다. (다형성)

 

파이프라인을 구성하면 Sink는 자신의 왼쪽에 있는 Source에 대한 포인터를 가지고 있으며

그 포인터를 이용해 해당 Source의 getNextFrame함수를 호출함으로써

아래와 같이 그 Source에 데이터를 받을 Sink의 버퍼

데이터를 받고나서 실행될 콜백함수의 포인터인 afterGettingFunc

Source가 더이상 데이터를 만들어 내지 않을때 실행될 콜백의 함수포인터인 onCloseFunc등을 전달한다.

 

<코드가 잘 안보이는 건 기분탓이다. =_=>

 

그러면 Source에서는 전달받은 정보를 모두 자신의 멤버변수에 저장하고

doGetNextFrame을 통해 실제 데이터를 전달 받는 동작을 수행한다.

 

doGetNextFrame은 virtual로 선언되어 있는 순수 추상함수인데

그 이유는 이 Live555에서 제공하는 Source의 데이터가 여러 데이터원에 기인하기 때문이다.

H.264데이터도 있고, 263도 있고, 데이터 종류에 따라 데이터를 전달하기 이전에 수행해야 할 동작들이 많이 때문에

아래에 보이는 엄청나게 많은 Source원에서 각각 이 doGetNextFrame()을 override해서 필요한 동작들을 수행한다

 

 

 

<겁나게 많다 하지만 모두 다 알 필요는 없다>

 

아울러 이 doGetNextFrame에는 자신의 앞쪽에 있는 또다른 Source

로부터 다시 데이터를 수신하는 getNextFrame()을 호출하고 이 과정이 계속 반복되면서

최종적으로 Sink에 데이터가 전달된다.

 

그리고 각 단위마다 데이터를 전달받고 나서 실행되는 afterGettingFrame함수가 있어서

자신의 앞 블록으로부터 데이터를 받으면, 아래 코드와 같이 afterGettingFunc 콜백함수 포인터를

호출해 다음 블록에 데이터가 왔음을 알리게 된다.

 

(이 함수포인터는 getNextFrame때 자신의 오른쪽 블록으로부터 넘겨 받았었다.)

 

 

 

사실 이런 사항은 그다지 자세히 할 필요는 없다. =_=

소스코드가 잘 구현되어 있기 때문에 그닥 변경할 필요도 없을 뿐더러, 

LGPL라이선스로 이 코드를 사용하고 싶다면 이 소스코드를 변경해서도 안된다.

 

다만 이런 사항을 잘 알고 있다면, 자신만의 기능을 가지고 있는

Source나 Filter 혹은 Sink등을 만들어 사용할 때 좀더 도움이 될 수 있을 것 같다.

 

참고로 필자는 H.264데이터를 디코딩해서 글씨를 씌우고 다시 H.264로 인코딩 한 데이터를 RTPSink에 전달하는

Filter를 만들어서 사용하고 있는데 이런 구조를 이해하는게 썩 많은 도움이 되었다.

Posted by 굿쟌
,

Live555을 이용해서 RTSP로 전달되는 (엄밀히 말하자면, RTP로 전달되는) H264 스트림을 전달 받는 방법을 알 게 되었다. 그런데 막상 받아놓은 바이트 배열을 디코딩 하려고 보니 이 배열의 어디부터 어디까지를 넣어야 하는지가 막막했다. Live555에서도 h264FramedSource와 같은 소스 필터 클래스를 제공하긴 하지만 H.264자체에 대해서는 완전 까막눈이이다보니 어떻게 활용을 해야 할지는 더 막막해 차라리 디코딩을 위해 공부해야 하는 FFmpeg부터 역으로 접근하면서 H.264에 대해 공부하면 어떨까 하고 FFmpeg부터 공부하기로 결심했다.

 

사실 이전에 XProtect라고 하는 Miliestone사의 VMS로부터 한프레임의 H.264 데이터를 통으로 받아 디코딩 해본 경험은 있지만 막상 다시 새로운 프로젝트를 만들어서 공부를 하려고 하니 어떻게 FFmpeg를 Visual studio 2010에 연동했었는지 가물가물했다. 그래서 아예 이 과정을 정리해 공유하면 나도 나중에 참고해서 사용 할 수 있고, 다른사람들도 보고 좋을거 같아 연동하는 과정을 기록해 보고자 한다.

 

Step 1. 일단 라이브러리를 다운 받아야 된다.

 

물론 FFmpeg는 오픈 소스이니까 소스코드를 다운 받고, mingw와 msys으로 gcc를 활용할 수 있는 환경을 만들어 직접 빌드해 사용 할 수 있다. 그러나 그냥 나같이 라이트하게 사용만 하고자 하는 목적이라면, 이 과정이 그닥 정신건강에 유익하지 않다. 소스 코드를 컴파일 하다가 에러 하나가 띡 하고 올라와 구글링해서 고치면 다른 에러가 띡 올라오고 그 과정에서 멘탈이 남아나질 않는다.

 

<에러와 함께 내 멘탈도 부서졌다>

 

 

하지만 인터넷 세상에는 착한 사람들이 참 많다.

https://ffmpeg.zeranoe.com/

 

요기에 가보면 어떤사람이 windows에서 사용할 수 있는 FFmpeg의 빌드 결과물을 그것도 32bit, 64bit 각자의 구미에 맞게 제공하고 있다. 홈페이지에서 build탭으로 이동해 보면 현재 최신버전.. 인지는 모르겠지만, 꾸준히 최신버전의 FFmpeg버전의 빌드를 제공하는지 제법 최신 버전의 FFmpeg빌드를 제공하고 있다. 이글을 작성하는 시점으로는 git-3ecc063을 빌드해서 올려놨다.

 

 

 

 

32bit, 64bit으로 빌드 해 놓은 것은 각자가 만들고 싶은 프로그램의 빌드 타겟과 일치하는 버전의 바이너리를 다운 받으면 되겠고... Static, Shared, Dev를 설명하자면.. 음.. 일단 ffmpeg는 라이브러리가 아니다 원래는 콘솔프로그램인데 이 콘솔 프로그램이 사용하는 많은 외부 라이브러리가 있다. 그 외부 라이브러리들을 이용해서 ffmpeg라는 콘솔프로그램이 뭐 미디어 파일을 변경하고 하는 일을 하는것이다.

 

그래서 이 ffmpeg라는 콘솔프로그램을 만들때 이 외부라이브러리 소스코드를 한데다가 싹 다 모아서 하나의 exe로 만들것인지 아니면 외부라이브러리를 dll형태로 빼 내어 다소 많은 소스코드가 들어간 exe를 가볍게 할 것인지 결정할 수 있다. 전자는 static이라고 부르고 후자는 shared라고 부른다. 그래서 각각 받아보면 static은 exe 3개만 떨렁 들어 있고, shared에는 dll파일 여러개와 exe파일 3개가 들어있다.

 

마지막으로 Dev는 shared에 들어 있는 dll파일을 개발자가 사용할 수 있도록 헤더파일과 lib파일을 제공하는데 이 파일들을 이용해서 FFmpeg가 가지고 있는 기능들을 개발자가 직접 사용할 수 있게 되는 것이다.

 

뭐 어찌됐든 다 받자. 공짜다.

 

Step 2. Visual sutdio 2010 프로젝트에 연동하기

 

아래 파일들은 shared를 눌러서 다운 받은 파일들이다. ffmpeg.exe, ffplay.exe, ffprobe.exe는 모두 다른 dll로 모듈화 되어 있는 기능을 참조하여 가져다 쓴다. 우리가 visual stduio 2010 프로젝트에 연동해야 하는 기능들도 이 dll파일들에 들어 있으니 이 dll파일을 가져다가 visual studio 프로젝트의 실행파일이 생기는 폴더로 몽땅 이동시키자.

 

<다운로드 싸이트에서 shared를 선택해 다운 받은 파일들>

 

다음으로는 프로젝트에 헤더파일과 라이브러리 파일을 가져다가 사용할 차례다. 헤더파일과 lib파일은 Dev폴더의 lib, include폴더에 들어 있다. visual studio 2010프로젝트가 해당 헤더와 lib파일의 위치를 알 수 있도록 아래와 같이 Visual studio 2010의 프로젝트 속성에서 포함파일 위치랑 라이브러리 파일 위치를 지정해준다.

 

 

<추가 포함 디렉터리>

 

<추가 라이브러리 디렉터리>

 

다음으로는 이제 헤더와 lib파일의 위치를 알려줬으니까 직접 포함도 해보고 lib파일을 연결시켜보자.

콘솔 main()함수가 있는 파일에 쓰든지 아니면 미리컴파일된 헤더에 쓰든지 적당한 곳에 아래 구문을 넣으면 된다.

 

extern "C" //FFmpeg가 C라이브러리이기 때문에 이부분이 필요하다.

{

#include "libavcodec\avcodec.h"
#include "libavdevice\avdevice.h"
#include "libavfilter\avfilter.h"
#include "libavformat\avformat.h"
#include "libavutil\avutil.h"
#include "libpostproc\postprocess.h"
#include "libswresample\swresample.h"
#include "libswscale\swscale.h"

}

 

#pragma comment ( lib, "avcodec.lib" )
#pragma comment ( lib, "avdevice.lib")
#pragma comment ( lib, "avfilter.lib")
#pragma comment ( lib, "avformat.lib")
#pragma comment ( lib, "avutil.lib")
#pragma comment ( lib, "postproc.lib")
#pragma comment ( lib, "swresample.lib")
#pragma comment ( lib, "swscale.lib")

 

복잡해 보이지만 복잡할 것 없다.

그냥 include 폴더에 들어 있는 폴더 이름이랑 같은 헤더들 다 포함시키고 동일 이름의 lib파일을 다 연결시킨 것이다.

 

Step 3. 드디어 컴파일

 

자 그럼 이제 컴파일 해보자.

그러면???

아래와 같은 에러가 발생한다.

 

 

위 에러는 헤더파일 폴더에 있는 libavutil폴더의 common.h이라는 헤더파일에서 inttypes.h라는 헤더를 포함하고 있는데 윈도우 os에서는 해당 헤더가 없기 때문에 발생하는 에러이다.. 어떻게 해결할지 구글님께 물어보자

 

stackoverflow에 보면 같은 증상을 호소하는 사람이 있다. 이 사람에게 다른사람이 알려준 inttypes.h헤더 파일이 있는데 이를 다운받아서 설치하도록 하자.

 

 

<동일한 병을 호소하는 환자 1>

 

 

<처방전>

 

this file은 여기서 다운 받을 수 있다.

http://code.google.com/p/msinttypes/downloads/list

 

여튼 이걸 다운 받고 이전단계에서 설정했던 포함 파일 디렉터리에다가 복사하자. 그리고 나서 아래와 같이 libavutile/common.h의 #include <inttypes.h>를 아래와 같이 수정해서 다운받은 헤더파일을 포함하자.

참고로 < >가 아니라 " "포함시켰다. 사용자가 만든 헤더는 " "로 포함시켜야 한다.

 

 

 

그리고 나서 아래 코드를 적어서 빌드를 해보자. 빌드가 잘 된다.

 

 

그런데 문제가 있다. 디버그 모드에서는 빌드도 되고 실행도 잘 되는데 Release모드에서는 빌드는 성공하지만 실행시키면 프로그램이 죽어버리는 것이다.이 문제에 대해서는 여러곳에서 구글링을 해봤는데 Release모드에서 다음과 같이 최적화 옵션을 변경하면 해결되는 것을 확인했다.

 

 

 

이 에러는 각 옵션이 활성화 되어 있을 때 최적화 과정에서 원치 않는 작동을 유발하여 발생하는 에러로 보인다. 그렇기 때문에 위와 같이 각 COMDAT, 참조 최적화 옵션을 disable시키면 더이상 이 에러가 발생하지 않는다.

(참고로 다른 싸이트에서 보면 프로젝트의 캐릭터 셋을 not Set으로 설정해 놓아야 한다고 하는 곳도 있는데, 유니코드로 설정해 두어도 문제는 없을 것으로 보인다.)

 

자 이제 FFmpeg를 가지고 놀 수 있는 환경이 되었다. GoGo

Posted by 굿쟌
,