2016년 2월 22일 월요일

TDD 란 ( Test_driven development )


Test-driven development (TDD) 는 매우 짧은 개발 서클의 반복에 의존하는 소프트웨어 개발 프로세스 이다. 우선 개발자는 요구되는 새로운 기능에 대한 자동화된 테스트 케이스를 작성하고, 해당 테스트를 통과하는 가장 짧은 코드를 작성한다. 그리고나서, 표준에 맞게 새로운 코드를 리팩터(refactor) 한다. 

TDD는 1999년도 애자일 기반 방법론이 자리 잡을 때 처음 소개가 되었다. 이 그룹에서는 실제 프로그래밍 전에 테스트 코드를 먼저 작성하면 어떨까 라고 생각하고 시도를 해 보았는데 퀄리티가 꽤 인상적이라 애자일 방법론에 TDD가 도입되기 시작했다. 즉, TDD를 좀더 알기쉽게 해석하자면 '테스트 먼저 개발'로 바꾸어 이해할 필요가 있다. 



1. Add a test

: 테스트 주도형 개발에선, 새로운 기능을 추가하는 것은 우선 테스트를 작성하는 것 에서부터 시작한다. 테스트를 작성하기 위해선, 개발자는 해당 기능의 요구사항과 명세를 분명히 이해하고 있어야 한다. 이는 사용자 케이스와 사용자 스토리 등으로 이해할 수 있으며, 테스팅 프레임워크가 소프트웨어 환경에 적합한가를 따지기도 전에 테스트를 작성할 수 있도록 해준다. 이는 또한 기존 테스트의 변형일 수도 있다. 이는 코드를 작성한 뒤 유닛테스트를 하는것과는 다른 특징이다. 즉, TDD는 개발자가 코드를 작성하기 전에 보다 요구사항에 있어 집중할 수 있도록 도와주는데, 이는 작지만 중요한 차이를 만들어준다. 즉, 기존의 프로세스는 코드를 작성하기 전에 '디자인'에 충분한 시간을 투자하면서 고려하지 못하지만, TDD의 경우 보다 정확한 프로그래밍 목적을 '디자인' 단계에서 반드시 미리 정의해야만 하고, 또 무엇을 테스트해야 할지 정의해야만 한다. 

2. Run all tests and see if the new one fails


: 이것은 test harness 가 잘 작동하도록 만들어준다. 즉, 새로운 테스트 코드는 새로운 동작코드를 항상 요구하게 되기 때문에 새로운 동작코드를 포함하지 않아서 실수할 수 있는 확률을 원천적으로 차단한다. 이 단계에서 테스트를 스스로 테스트를 수행하는데, 새로운 테스트가 항상 잘 통과되는 가능성을 배재함으로써 새로 추가된 테스트에서 잘못된 점을 잡아낼 수 있도록 한다. 또한 새로운 테스트는 예상되는 이유에 대해 반드시 실패해야 한다. 이를통해 개발자는 유닛 테스트가 제약조건에 의해 (즉, 의도된 케이스에 대해서만) 적절히 테스팅 되었다는 자신감을 얻게된다.


3. Write some code


: 이 다음 순서는 위에서 작성한 테스트를 통과하는 코드를 작성한 일 이다. 이 단계에서 작성된 코드는 완전하지 않을 것 이다. 물론 테스트를 통과하긴 하겟지만, 이 코드는 차후에 더 향상될 가능성을 지니고있다.



4. Run tests


: 이제 모든 테스트들이 통과할 것 이다. 이제 개발자는 새로운 코드가 테스트의 요구조건에 부합하며 기존의 작성했던 기능에 대해 충돌이 일어나지 않는다는 사실에 자신감을 가질 수 있을 것 이다. 만일 그렇지 않다면 그렇게 되도록 끊임없이 코드를 수정해야 한다.


5. Refactor code


: 점점 커저가는 코드는 TDD 개발기간 동안에 정기적으로 clean up 되어야 한다. 새롭게 추가한 코드는 원래의 테스트를 통과하는 목적에서 출발하여 논리적으로 제자리에 올 수 있도록 개선되어야 한다. 중복을 제거하고, 각각의 네이밍이 목적을 분명히 할 수 있도록 수정해야 한다. 기능이 추가되면, 메소드 몸체는 좀더 커질 수 있고, 다른 오브젝트도 또한 몸집이 커질 수 있다. 이것들을 좀더 쪼개어 가독성과 유지보수에 좀더 이득을 취할 수 있으며, 상속 계층구조를 더 논리적으로 변환시키기 위해 재정렬 시킬 수 있다. 이러한 작업들은 소프트웨어 라이프사이클에 있어 굉장히 큰 가치를 제공할 것 이다. 클린 코드를 작성하기 위한 리펙토링 가이드라인이 존재한다. (Beck, Kent (1999). XP Explained, 1st Edition. Addison-Wesley Professional. p. 57 , Ottinger and Langr, Tim and Jeff. "Simple Design". Retrieved5 July 2013. ) . 각 리펙토링 단계마다 테스트 케이스가 반복적으로 수행되기 떄문에, 개발자는 해당 프로세스가 기존의 다른 기능들을 변경하지 않는다는 확신을 가실 수 있게된다.


- Repeat


: 또다른 기능을 추기하기 위한 새로운 테스트를 시작하면서, 위의 사이클은 다시한번 반복된다. 해당 테스트 사이클은 적당히 작아야 하며, 새로운 코드가 새로운 테스트를 빠르게 통과하지 못했을 경우 다시 이전 단계로 회귀할 것 이다. 즉, 이와같은 지속적 통합은 언제나 되돌아갈 수 있는 회귀점을 제공해준다. 외부 라이브러리를 사용할 경우, 특별히 외부 라이브러리가 버그가 있을것 이라는 의심이 들지 않는경우, 해당 라이브러리를 테스트하는데에 너무 큰 부분을 차지하지 않는것이 좋다.



TDD의 장점


TDD의 가장 큰 장점은 높은 퀄리티의 소프트웨어를 보장한다는 것이다. 에러나 버그가 없으며, 추가적인 요구사항이 있을 때 손쉽게 그 요구사항을 반영해줄 수 있으며, 누가 그 코드를 봐도 손쉽게 이해하고 수정할 수 있어야 한다는 것이다. 

보다 튼튼한 객체지향적인 코드 생산 가능

테스트 코드를 먼저 작성한다는 것은 하나하나의 기능들에 대해서 철저히 구조화 시켜 코드를 작성한다는 것을 뜻한다. TDD는 “모든 코드”가 재사용성 기반으로 작성되어야 하기 때문에 보다 튼튼한 코드 생산이 이루어 지게 된다.

재설계 시간의 단축

앞에서 설명한 것처럼 테스트 코드를 먼저 작성하기 때문에 내가 지금 무엇을 해야 하는지 분명히 정의를 하고 시작하게 된다. 테스트 코드를 작성하면서 인터페이스나 클래스의 구조들을 많이 수정하게 된다. 만약 테스트 코드가 아니라 실제 구현 코드를 작성하면서 이 작업을 할 경우에 구현하고 있는 코드들 또한 고쳐야 하는 문제를 유발하게 된다. 실제로 초급 개발자와 중고급 개발자의 차이는 얼마만큼 예외 상황을 많이 알고 있느냐의 차이로 나누어 지기도 한다. 여기서 만약 먼저 테스트 시나리오들을 정의하게 되면 그만큼 필요한 예외 상황들을 먼저 선임들과 상의할 수 있다는 것이고, 초급 개발자들도 예외 상황들과 테스트 되어야 하는 가능성들을 먼저 조사하게 되는 것이다.

디버깅 시간의 단축

이것은 통합 테스팅이 아닌 유닛 테스팅을 하는 이점이기도 하다. 아래서 보다 정확한 논리를 설명할 것이지만 먼저 간단하게 설명하자면 우리는 각각의 모듈 별로 테스트를 자동화할 수 있는 코드가 없다면 특정 버그를 찾기 위해서 모든 레벨(레이어)의 코드들을 살펴봐야 한다. 예를 들어 사용자의 데이터가 잘못 나온다면 DB의 문제인지, 비즈니스 레이어의 문제인지 UI 단의 문제인지 실제 모든 레이어들을 전부다 디버깅할 필요 없이 자동화 된 유닛테스팅 결과로 우리는 손 쉽게 찾아 낼 수 있다는 것이다.

테스트 문서의 대체가능

주로 S.I 프로젝트를 진행하다 보면 어떤 요소들이 테스트 되었는지 테스트 정의서를 만들고는 한다. 이것은 단순 통합테스트 문서에 지나지 않는다. 즉, 내부적인 하나하나의 로직들이 어떻게 테스트 되었는지 제공할 수 없다. 하지만 TDD를 하게 될 경우에 테스팅을 자동화 시킴과 동시에 보다 정확한 테스트 근거를 산출할 수 있다.

추가 구현의 용이함


개발 뒤에 어떤 기능을 추가할 때 가장 우리를 번거롭게 하는 것이 이 기능이 기존의 코드들에 얼만큼 영향을 미치게 될지 모르기 때문에 다시 모든 코드들을 테스트 해야 되는 것이 큰 곤욕이다. 역시나 바로 뒷 부분에 살펴볼 내용이지만 유닛 테스트로 자동화 될 경우에 우리는 수동으로 모든 테스트를 다시 진행해야 되는 시간적인 낭비를 줄일 수 있다.

놓치기 쉬운 C++ 디테일들


  • malloc이 생성자를 호출하지 않는 것과 마찬가지로 free는 파괴자를 호출하지 않는다. 객체를 동적 할당할 때는 반드시 new/delete 연산자를 사용하여 생성자와 파괴자를 적절하게 호출하도록 해야 한다.



  • 정수 하나를 인수로 취하는 생성자가 정의되어 있다면, 이 클래스는 디폴트 생성자를 가지지 않는다. 이 경우 디폴트 생성자 선언문은 적절한 생성자를 찾을 수 없으므로 에러로 처리될 것이다. 별도의 생성자를 제공했다는 것은 클래스를 만든 사람이 이 객체는 이런 식으로 초기화해야 한다는 것을 분명히 명시한 것이므로 컴파일러는 이 규칙을 어긴 코드에 대해 사정없이 에러로 처리한다. 디폴트 생성자가 없는 클래스는 객체 배열을 선언할 수 없다. (이건 가능 Position There[3]={Position(1,2,'x'),Position(3,4,'y'),Position(5,6,'z')};)   



  •  ' Person Boy("강감찬",22); Person Young=Boy; ' 이 코드는 정상적으로 컴파일되며 실행도 되지만 종료할 때 파괴자에서 실행중 에러가 발생하는데 왜 그런지 보자. Young 객체가 Boy객체로 초기화될 때 멤버별 복사가 발생하며 Young의 Name멤버가 Boy의 Name과 동일한 번지를 가리키고 있다. 두 객체가 같은 메모리를 공유하고 있기 때문에 한쪽에서 Name을 변경하면 다른 쪽도 영향을 받게 되어 서로 독립적이지 못하다. 이 객체들이 파괴될 때 문제가 발생하는데 각 객체의 파괴자가 Name 번지를 따로 해제하기 때문이다. new는 Boy의 생성자에서 한 번만 했고 delete는 각 객체의 파괴자에서 두 번 실행하기 때문에 이미 해제된 메모리를 다시 해제하려고 시도하므로 실행중 에러가 된다. 이 문제를 해결하려면 초기화할 때 얕은 복사를 해서는 안되며 깊은 복사를 해야 하는데 이때 복사 생성자가 필요하다. 얕은 복사가 문제의 원인이었으므로 깊은 복사를 하는 복사 생성자를 만들어 해결할 수 있다.
    Person(const Person &Other)   {
        Name=new char[strlen(Other.Name)+1];
        strcpy(Name,Other.Name);
        Age=Other.Age; } 
복사 생성자는 자신과 같은 타입의 다른 객체에 대한 레퍼런스를 전달받아 이 레퍼런스로부터 자신을 초기화한다. Person복사 생성자는 동일한 타입의 Other를 인수로 전달받아 자신의 Name에 Other.Name의 길이만큼 버퍼를 새로 할당하여 복사한다. 새로 메모리를 할당해서 내용을 복사했으므로 이 메모리는 완전한 자기 것이며 안전하게 따로 관리할 수 있다. 복사 생성자의 인수는 반드시 객체의 레퍼런스여야 하며 객체를 인수로 취할 수는 없다. 포인터 형식도 안된다.


  • 함수의 인수로 객체를 넘기는 경우는 아주 흔한데 이때도 복사 생성자가 호출된다. 함수 호출 과정에서 형식 인수가 실인수로 전달되는 것은 일종의 복사생성이다. 함수 내부에서 새로 생성되는 형식인수 AnyBody가 실인수 Boy를 대입받으면서 초기화되는데 이때 복사 생성자가 없다면 AnyBody가 Boy를 얕은 복사하며 두 객체가 동적 버퍼를 공유하는 상황이 된다. AnyBody는 지역변수이므로 PrintAbout 함수가 리턴될 때 AnyBody의 파괴자가 호출되고 이때 동적 할당된 메모리가 해제된다. 이후 Boy가 메모리를 정리할 때는 이미 해제된 메모리를 참조하고 있으므로 에러가 발생할 것이다. 복사 생성자가 정의되어 있으면 AnyBody가 Boy를 깊은 복사하므로 아무런 문제가 없다.
void PrintAbout(Person AnyBody) {
 AnyBody.OutPerson(); }


  • 멤버에 단순히 값을 대입하기만 하는 경우 본체에서 = 연산자를 쓰는 대신 초기화 리스트(Member Initialization List)라는 것을 사용할 수 있다.
Position(int ax, int ay, char ach) : x(ax),y(ay),ch(ach){
     // 더 하고 싶은 일 }
상수는 선언할 때 반드시 초기화해야 한다. const int year=365; 의 형식으로 상수를 선언하는데 여기서 =365를 빼 버리면 다시는 이 상수값을 정의할 수 없으므로 에러로 처리된다. 단, 클래스의 멤버일 때는 객체가 만들어질 때까지 초기화를 연기할 수 있으며 생성자의 초기화 리스트에서만 초기화 가능하다. 
레퍼런스 멤버는 다음과 같이 대입 연산자로 초기화할 수 없다. 포함된 객체를 초기화할 때도 초기화 리스트를 사용한다.
class Some {
        int &ri;
Position Pos;
Some(int &i,int x,int y) { ri=i; Pos(x,y); } // correction ---> Some(int &i) : ri(i),Pos(x,y) { }
}


  • 프렌드 지정은 전이(A->B->C != A->C)되지 않으며 친구의 친구 관계는 인정하지 않는다.friend class B, C; 이런 문법은 허용되지 않는다. 프렌드 관계는 상속되지 않는다.



  • Num은 여전히 Count 클래스 내부에 선언되어 있되 static 키워드를 붙여 정적 멤버임을 명시했다. 클래스 선언문에 있는 int Num; 선언은 어디까지나 이 멤버가 Count의 멤버라는 것을 알릴 뿐이지 메모리를 할당하지는 않는다. 그래서 정적 멤버 변수는 외부에서 별도로 선언및 초기화해야 한다. Count 클래스 선언문 뒤에 Num 변수를 다시 정의했는데 이때 반드시 어떤 클래스 소속인지 :: 연산자와 함께 소속을 밝혀야 한다. 외부 정의에 의해 메모리가 할당되며 이때 초기값을 줄 수 있다.
class Count {
    private:
int Value;
static int Num;
public: static void InitNum() { }; };
int Count::Num=0; 
정적 멤버 변수는 객체와 논리적으로 연결되어 있지만 객체 내부에 있지는 않다. 정적 멤버 변수를 소유하는 주체는 객체가 아니라 클래스이다. 그래서 객체 크기에 정적 멤버의 크기는 포함되지 않으며 sizeof(C) = sizeof(Count)는 객체의 고유 멤버 Value의 크기값인 4가 된다.


  • 정적 멤버 함수는 정적 멤버만 액세스할 수 있으며 일반 멤버(비정적 멤버)는 참조할 수 없다. 왜냐하면 일반 멤버 앞에는 암시적으로 this->가 붙는데 정적 멤버 함수는 this를 전달받지 않기 때문이다. 정적 멤버 함수인 InitNum에서 비정적 멤버인 Value를 참조하는 것은 불가능하다.
static void InitNum() {
     Num=0;
     Value=5; // Value를 불법으로 참조했다는 에러 메시지가 출력된다.
}


  • 상수 멤버 함수는 멤버값을 변경할 수 없는 함수이다. 멤버값을 단순히 읽기만 한다면 이 함수는 객체의 상태를 바꾸지 않는다는 의미로 상수 멤버 함수로 지정하는 것이 좋다. 클래스 선언문의 함수 원형 뒤쪽에 const 키워드를 붙이면 상수 멤버 함수가 된다. 함수의 앞쪽에서는 리턴값의 타입을 지정하기 때문에 const를 함수 뒤에 붙이는 좀 별난 표기법을 사용한다.
     int GetValue() const;             // 상수 멤버 함수
단, 상수 멤버 함수라도 정적 멤버 변수의 값은 변경할 수 있는데 정적 멤버는 객체의 소속이 아니며 객체의 상태를 나타내지도 않기 때문이다.
     const Position There(3,4,'B');
     There.MoveTo(40,10);           // 에러 발생
 There는 상수 객체로 선언되었므로 상수 멤버 함수인 OutPosition만 호출할 수 있으며 MoveTo 호출문은 에러로 처리된다.

  • 다형성 : 재정의될 수 있는 부모클래스의 메서드를 가상화 함으로써 , 차후에 특정 메서드를 동작시켰을 때 경우에 따라 다른 동작을 할 수 있는 능력. ( 부모 클래스형의 포인터로부터 멤버 함수를 호출할 때 비가상 함수는 포인터가 어떤 객체를 가리키는가에 상관없이 항상 포인터 타입 클래스의 멤버 함수를 호출한다. 반면 가상 함수는 포인터가 가리키는 실제 객체의 함수를 호출한다는 점이 다르다. 그래서 파생클래스에서 재정의하는 멤버 함수, 또는 앞으로라도 재정의할 가능성이 있는 멤버 함수는 가상으로 선언하는 것이 좋다. 그래야 부모 클래스의 포인터 타입으로 자식 객체의 멤버 함수를 호출해도 정확하게 호출된다. )

  • 동적 바인딩 : 컴파일러는 gotoxy 함수가 어떤 주소에 있는지 알고 있으며 그래서 gotoxy 호출문을 이 함수의 주소로 점프하는 코드로 번역할 것이다. 컴파일하는 시점(정확하게는 링크 시점)에 이미 어디로 갈 것인가가 결정되는 이런 결합 방법을 정적 결합(Static Binding) 또는 이른 결합(Early Binding)이라고 한다. 결합(Binding)이란 함수 호출문에 대해 실제 호출될 함수의 번지를 결정하는 것을 말하는데 지금까지 작성하고 사용했던 일반적인 함수들은 모두 정적 결합에 의해 번역된다. 그런데 가상 함수는 포인터가 가리키는 객체의 타입에 따라 호출될 실제 함수가 달라지므로 컴파일시에 호출할 주소가 결정되는 정적 결합으로는 정확하게 호출할 수 없다. 왜냐하면 포인터가 실행중에 어떤 타입의 객체를 가리킬지 컴파일 중에는 알 수 없기 때문이다.  실행중에 호출할 함수를 결정하는 이런 결합 방법을 동적 결합(Dynamic Binding) 또는 늦은 결합(Late Binding)이라고 한다. 이렇게 해야 전달된 객체에 따라 각기 다른 동작을 할 수 있는 다형성을 구현할 수 있다. 동적 결합은 멤버 함수를 포인터(또는 레퍼런스)로 호출할 때만 동작한다.


  • 가상 함수 :  "동적 결합 함수". 이 함수에 virtual이라는 용어를 쓴 이유는 전통적인 함수처럼 정적 결합을 하지 않으며 파생 클래스가 재정의해도 안전하다는 뜻이다. ( 즉, 래퍼런스로써 부모 포인터가 자식 클래스를 가리키고 있을 경우, 동적 결합에 의해 , 부모 메서드를 오버라이딩 하고있는 자식 메서드가 동작할것을 약속한다. )


  • 파괴자를 가상함수로 선언하지 않는것은 잠재적으로 메모리 누수를 일으킬 여지가 있다.

  • 가상 함수를 써야할 경우 1. 해당 클래스가 자식 클래스로 파생될 가능성이 조금이라도 있을경우 2. 파생 클래스에서 함수의 동작을 재정의할 가능성이 조금이라도 있는경우 3. 부모 클래스 타입의 포인터로부터 자식 클래스가 호출당할 가능성이 조금이라도 있는경우.



  • 순수 가상 함수 : 파생 클래스에서 반드시 재정의해야 하는 함수이다. 순수 가상 함수는 일반적으로 함수의 동작을 정의하는 본체를 가지지 않으며 따라서 이 상태에서는 호출할 수 없다. 본체가 없다는 뜻으로 함수 선언부의 끝에 =0이라는 표기를 하는데 이는 함수만 있고 코드는 비어 있다는 뜻이다 " virtual void Draw()=0; "하나 이상의 순수 가상 함수를 가지는 클래스를 추상 클래스(Abstract Class)라고 한다. 추상 클래스는 동작이 정의되지 않은 멤버 함수를 가지고 있기 때문에 이 상태로는 인스턴스를 생성할 수 없다.








2016년 2월 21일 일요일

MPEG-4 Part 10 (H.264/AVC) - NAL(Network Abstraction Layer) 분석

Introduction

Video Coding Layer

: H.264/AVC 표준은 애초부터 다양한 네트워크 전송환경에 대응하기 위해 NAL 포맷을 적용하였다. ( 적절한 헤더정보와 함께, VCL(video coding layer) 포맷으로 비디오를 표현하는 방식 ) 즉, NAL은 "network friendliness" 를 표방하여, 단순하고 효과적으로 VCL을 커스터마이징 할 수 있게 한다. 
 즉 단순히 얘기하면, 비디오 인코딩/디코딩 과정을 네트워크 어플리케이션에 최적화(좀 더 유연하고, 간편하게) 되도록 만든 작업이다. 


avc/h264 stream 구성


NAL : Network Abstraction Layer
NAL = NAL unit + RBSP
NAL unit : start prefix
    Network Coding Layer
  • SPS : 프로파일 , 레벨 ,해상도 , 포맷 등 파일 전체에 대한 포괄적인 정보가 부화화하여 저장되어 있다. ( 00 00 00 01 67  ~ )
  • PPS : PPS는 SPS가 정의하는 내용보다 조금 더 세부적인 내용을 포함한다. 디코딩되는 픽처에 적용되는 파라미터를 포함한다. ( 00 00 00 01 68 ~ )
  • IDR Picture :  디코딩에대한 기본정보가 초기화되는 Picture 이다.  Reference 이미지를 사용하지 않고 독자적으로 디코딩 될 수 있는 Picture. I-slice 나 SI-slice 를 나타낸다. ( 00 00 00 01 65 ~ ) ( IDR picture 앞에는 반드시 SPS , PPS 정보가 들어가 있어야 한다. )

NAL types


NAL types table 에서 나타낸 types number 는 실제 byte stream 을 열어보면


AUD : 00 00 00 01 09
SPS : 00 00 00 01 67
PPS : 00 00 00 01 68


이런식으로 나타난다. 00 00 00 01 이 일종의 구분자가 되기 때문이다. 즉, SPS NAL 을 표현하고 싶다면, 해당 NAL unit 에 00 00 00 01 67을 적어주고, RBSP 부분에 SPS 정보를 적어주면 된다. 

Slice는 마찬가지로 NAL 의 일종인데, 실제로 picture를 저장하는 부분이며, 헤더와 Micoblock로 이루어진 data 파트로 나뉘어진다. 익히 아는 I,B,P picture ( 이전 게시물을 참조 )를 포함한 정보를 담고있다.





Slice types
더 자세한 정보 :
http://gentlelogic.blogspot.kr/2011/11/exploring-h264-part-2-h264-bitstream.html


ffmpeg 라이브러리를 활용하여 x264 규격으로 transcoding , live streaming server 제작 (encoding)

: 이미 디코딩 된 raw image를 encoding 하기 위해선, 필터링 ( 혹은 software scaling ) , 코덱 설정 등의 과정이 필요하다.

우선 코덱 format 을 지정할 컨텍스트와, 데이터를 받을 frame 과 packet 자료형이 필요하며, software scaling에 쓰일 자료형도 필요하다.

특이한 점은 raw 데이터를 받을 frame에 미리 buffer 를 연결해야 한다는 것 이다.

여기서 우리는 AVC 규격으로 인코딩 할 것임으로, x264 함수를 직접 작성하여 인코딩을 해보도록 한다. ( 원래는 ffmpeg 설치폴더에 libavutil/libx264.c에 작성되어있는 코드이지만, 필요한 부분만 발췌하여 사용하기 위해 따로 x264function.c를 만들었다. )

( 아래 예제 코드는 이전 게시물과 연동되어 사용됩니다. )


  • 자료형 선언
  AVCodecContext     *pCodecCtx = NULL;
  AVCodecContext     *pCodecCtxOut= NULL;
  AVFrame               *pFrameOut = NULL;
  AVPacket                packetOut;
  uint8_t                  *buffer = NULL;
  struct SwsContext    *sws_ctx = NULL;
  int                         frameFinished;
  int                         numBytes;
  int                         got_output;
  int                         frame_count;
  int                         i, videoStream;
  • 송출할 frame 과 buffer 를 연결한다.
  pFrameOut=av_frame_alloc();
  if(pFrameOut==NULL)
    return -1;
  // Determine required buffer size and allocate buffer
  numBytes=avpicture_get_size(AV_PIX_FMT_YUV420P, pCodecCtx->width,pCodecCtx->height);
  buffer=(uint8_t *)av_malloc(numBytes*sizeof(uint8_t));
  printf("Required buffer size : %d bytes\n",numBytes);
  avpicture_fill((AVPicture *)pFrameOut, buffer, AV_PIX_FMT_YUV420P,pCodecCtx->width, pCodecCtx->height);
 : picture를 AVC 규격으로 저장할 것 이기 떄문에 YUV420P로 저장할것을 약속한다.
  • 인코딩에 필요한 codec context 를 구성한다.
pCodec = avcodec_find_encoder(AV_CODEC_ID_H264);
if (!pCodec || pCodec==NULL) {
   fprintf(stderr, "Codec not found\n");
   exit(1);
pCodecCtxOut = avcodec_alloc_context3(pCodec);
if (!pCodecCtxOut || pCodecCtxOut==NULL) {
   fprintf(stderr, "Could not allocate video codec context\n");
   exit(1);
} // put sample parameters 
pCodecCtxOut->bit_rate = BIT_RATE;
pCodecCtxOut->width = WIDTH;
pCodecCtxOut->height = HEIGHT;
pCodecCtxOut->time_base = (AVRational){1,FPS};
pCodecCtxOut->gop_size = GOP_SIZE;
pCodecCtxOut->max_b_frames = 1;
pCodecCtxOut->pix_fmt = AV_PIX_FMT_YUV420P;
av_opt_set(pCodecCtxOut->priv_data, "preset", "slow", 0);
if(avcodec_open2(pCodecCtxOut, pCodec, NULL)<0) return -1;
pFrameOut->width = WIDTH;
pFrameOut->height = HEIGHT;
pFrameOut->format = pCodecCtxOut->pix_fmt;
ret = av_image_alloc(pFrameOut->data, pFrameOut->linesize, pCodecCtxOut->width,pCodecCtxOut->height,pCodecCtxOut->pix_fmt, 32);
if (ret < 0) {
fprintf(stderr, "Could not allocate raw picture buffer\n");
exit(1);  
}

코덱과 picture 를받을 frame 환경설정을 한다. BIT_RATE WIDTH HEIGHT FPS GOP_SIZE 등을 조절하여 AVC 의 프로필레벨을 조절할 수 있다.
( 필자는 800000, 176, 144, 15, 1로 하여 쓸만한 화질을 얻을 수 있었다.) 
  • X264 코덱 적용
X264_init(pCodecCtxOut);
: ffmpeg/libavcodec/libx264.c 에서 정의된 함수이다. 위에서 정의한 코덱 환경설정을 avc 코덱 규격에 맞게 적용시켜주는 역할을 한다. ( 인코딩 시 x264_frame 함수로 할 것 이기 때문에 규격을 맞춰주는것은 필수이다. )
  • software scaling 준비 ( 스케일러 환경설정 )
sws_ctx = sws_getContext(pCodecCtx->width,
pCodecCtx->height,
pCodecCtx->pix_fmt,
pCodecCtxOut->width,
pCodecCtxOut->height,
pCodecCtxOut->pix_fmt,
SWS_FAST_BILINEAR,
NULL,
NULL,
NULL
);
: 소프트웨어 스케일링이란 일종의 필터링 같은 개념이다. 디코딩되어 얻은 raw image를 인코딩 규격에 맞게 변환시켜 주는 과정이다. ffmpeg 는 스케일링 기능을 포함한 필터링 기능을 지원하지만, 우선은 스케일링만으로 사용해본다.
 
  • 인코딩 

while(av_read_frame(pFormatCtx, &packet)>=0 )
{
  if(packet.stream_index==videoStream)
  {
   // Decode video frame
   avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
   av_init_packet(&packetOut);
   fflush(stdout);
   if(frameFinished)
   {
   pFrameOut->pts=frame_count;
   // software scaleling
   sws_scale(sws_ctx, (uint8_t const * const *)pFrame->data,pFrame->linesize, 0, pCodecCtx->height,pFrameOut->data, pFrameOut->linesize);
   // Encode video frame by x264
   ret = X264_frame(pCodecCtxOut, &packetOut, pFrameOut, &got_output);
   if (ret < 0) {
     fprintf(stderr, "Error encoding frame\n");
     exit(1);
   }
   if (got_output) {
      // ... 패킷 처리 ( 전송 or 파일쓰기 ) ... //
     }
   }
   frame_count++;
   //To avoid memory leak
   av_free_packet(&packet);
   av_init_packet(&packet);
   av_free_packet(&packetOut);
   }
}
: 흐름을 간단히 설명하면 , 파일 읽음 -> 디코딩 -> 스케일링 -> 인코딩 -> 반복 , 으로 구성된다. 지난 글에서 디코딩까지 진행했음으로 인코딩 부분만 적용시키면 된다. 이미지 컨데이너는 packet -> frame -> FrameOut -> packetOut 순으로 변경된다. 메모리 leak을 피하기 위해 반복문이 끝나면 항상 packet을 free 해준다. got_output은 packet을 통해 인코딩된 이미지가 정상적으로 받았다는것을 의미함으로, got_output을 확인하여 패킷단위로 데이터를 처리하면 될 것 이다.
int state = sendto(pkt_sock,pPacket,packetOut.size+2,0,(struct sockaddr*)&client_addr,sizeof(client_addr));
필자는 sendto 로 전송하여 실시간 스트리밍서버를 구성하였다.



2016년 2월 18일 목요일

ffmpeg 라이브러리를 활용하여 x264 규격으로 transcoding , live streaming server 제작 (extract - decoding)


라즈베리 파이에 ffmpeg을 활용해 라이브 스트리밍 서버를 탑재하면서 배웠던 지식을 글로 남기고자 작성한다. 서버는 단순 udp서버임으로 생략하기로 하고, ffmpeg 라이브러리를 활용하여 decoding 하고 encoding 하는 과정을 소개해 보기로 한다.


환경


grep . /etc/*-release

PRETTY_NAME="Raspbian GNU/Linux 7 (wheezy)"
NAME="Raspbian GNU/Linux"
VERSION_ID="7"
VERSION="7 (wheezy)"
ID=raspbian
ID_LIKE=debian
ANSI_COLOR="1;31"
HOME_URL="http://www.raspbian.org/"
SUPPORT_URL="http://www.raspbian.org/RaspbianForums"
BUG_REPORT_URL="http://www.raspbian.org/RaspbianBugs"

라즈베리 파이 2로 작업하였다. 

update 시 공개키 문제가 있어서 

sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys
(or)
wget http://archive.raspberrypi.org/debian/raspberrypi.gpg.key
apt-key add raspberrypi.gpg.key
이렇게 해결하였다.

구성요소 : 라즈베리 파이2, 내부망, Usb camera
설치 라이브러리 : ffmpeg
ffmpeg 설치가이드 :
https://www.bitpi.co/2015/08/19/how-to-compile-ffmpeg-on-a-raspberry-pi/
꼭 x264 라이브러리도 함께 설치해 주길 바란다. 


  • 관련 헤더파일
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/common.h>
#include <libavutil/avutil.h>
#include <libavfilter/avfilter.h>
#include <libavutil/eval.h>
#include <libavutil/opt.h>
#include <libavutil/mem.h>
#include <libavutil/pixdesc.h>
#include <libavutil/stereo3d.h>
#include <libavutil/intreadwrite.h>
#include <libavutil/pixfmt.h>

ffmpeg 라이브러리를 사용하여 /dev/video0 이미지를 받아서 디코딩 해보자


  • 가장 기본적으로 필요한 자료구조를 선언한다
AVFormatContext   *pFormatCtx = NULL;
AVCodecContext    *pCodecCtxOrig = NULL;
AVCodec              *pCodec = NULL;
AVFrame              *pFrame = av_frame_alloc();
AVPacket               packet;
 각각이 어떤역할을 하는지 대략적으로 알아야한다. 다음 예제를 따라가며 짐작해보길 바란다.  
  • 등록을 수행한다. 
  av_register_all();
  avcodec_register_all();
  avdevice_register_all();
  • /dev/video 에서 받기위한 작업
  AVInputFormat *inputFormat = av_find_input_format("v4l2");
  AVDictionary *options = NULL;
  av_dict_set(&options,"framerate","25",0);
  int ret;
  ret = avformat_open_input(&pFormatCtx,"/dev/video0",inputFormat,&options);
  if(ret != 0)
        exit(1);
   이부분에서 ret 값이 -16이 되면서 실행이 안되는 경우가 있는데, 이는 usb camera가 이미 점유당하고 있다는 뜻이다. 꺼져있는게 분명한데 점유가 되있는 상황이 있는 경우 재량껏 해결하시길 바란다. ( dev/video0 권한을 775 로 수정하는것이 도움이 되었지만 완벽히 해결해주진 않았다. )
  • 스트림 정보를 추출
  if(avformat_find_stream_info(pFormatCtx, NULL)<0) {
    printf("Couldn't find stream information\n");
      return -1;
  }
  • 첫번째 비디오 스트림을 찾는다.
  int videoStream=-1;
  for(int i=0; i<pFormatCtx->nb_streams; i++)
    if(pFormatCtx->streams[i]->codec->codec_type==AVMEDIA_TYPE_VIDEO) {
      videoStream=i;
      break;
    }
  • 비디오 스트림에서 디코딩에 필요한 코덱 정보를 찾아낸다.
  pCodecCtxOrig=pFormatCtx->streams[videoStream]->codec;
  pCodec=avcodec_find_decoder(pCodecCtxOrig->codec_id);
  if(pCodec==NULL) {
    fprintf(stderr, "Unsupported codec!\n");
    return -1;
  }
  • 해당 코덱을 받아오자.
   pCodecCtx = avcodec_alloc_context3(pCodec);
  if(avcodec_copy_context(pCodecCtx, pCodecCtxOrig) != 0) {
    fprintf(stderr, "Couldn't copy codec context");
    return -1;
  }
  if(avcodec_open2(pCodecCtx, pCodec, NULL)<0)
    return -1;
  •  이제 디코딩을 시작하지
  while(av_read_frame(pFormatCtx, &packet)>=0 )//&& frame_count<=100 )
  {
    if(packet.stream_index==videoStream)
    {
      // Decode video frame
      avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);
/... Raw image in packet 처리 ( ex. 파일에 쓴다던지 ).../ 
    }
      av_free_packet(&packet); // memory leak을 피하기 위해
      av_init_packet(&packet); 
}
 Read 할 경우 pFormatCtx 의 정보를 바탕으로 packet 에 데이터를 넣는것을 추측할 수 있다. 이때 받는 이미지는 raw 이미지가 아니고 카메라 자체에서 나름대로 인코딩된 규격을 받아온다. ( 콘솔창에서 정보를 확인할 수 있다. 카메라마다 다르기때문에 일정한 규격으로 전송하기 위해선 transcoding 이 필요하다. )
 packet 으로 읽은 데이터를 pCodecCtx 정보를 바탕으로 pFrame에 디코딩 시킨다. 이때 받아온 데이터는 raw 이미지임을 알 수 있다. 디코딩 직후 frame에 남아있는 raw image data를 확인하고 싶다면 파일에 써서 보길 바란다.

  • 빠짐없이 free 해준다.
  av_frame_free(&pFrame);
  avcodec_close(pCodecCtxOrig);
  avformat_close_input(&pFormatCtx);
  av_free(pCodecCtxOrig);

비디오 format 이해

1. 컨테이너


동영상 파일은 재생과 편집을 월활하게 하기위한 일련의 규격을 담는다. 이러한 규격을 가진 파일을 컨테이너라 칭한다. 컨테이너는 하나이상의 스티림을 지니는데, 스트림은 시간에따라 변하는 비디오 혹은 오디오 데이터를 의미한다. 컨테이너에 스트림을 담는 과정을 멀티플랙싱(Muxing) 이라 하고, 반대로 추출하는 과정을 디멀티플랙싱(Demuxing) 이라 한다.

 컨테이너에 속한 정보

  • 메타 정보 (촬영날짜, 위치)
  • 가지고있는 스트림 갯수
  • 동영상 전체 길이
  • 자막 정보
  • 실시간 스트리밍을 위한 스트림 위치정보
ex ) MPEG-PS, MPEG-TS, MPEG-4 Part 14, QuickTime, 3GPP, Ogg, WebM ..

Container format

2. 코덱(codec)


아날로그 신호로 이루어진 비디오, 오디오 를 압축된 부호로 변환하기 위한 압축 규격

압축 : analogue-> digital :인코딩 (encoding)
복원 : digital -> analogue : 디코딩 (decoding)
재압축 : digital -> analogue -> digital : 트랜스코딩 (transcoding : format 변경을 위함)

ex ) MPEG-1 Part 2, MPEG-2 Part 2, MPEG-4, Divx, MPEG-4 part 10 (H.264/AVC)..

3. 비디오 압축


비디오는 수많은 프레임이 시간축을 기준으로 모여 저장된 것이다. 이러한 프레임을 하나하나 개별적으로 압축한다면 용량낭비가 심할 것 이다. 따라서 비디오를 압축할 경우 하나의 프레임과 주변프레임의 상관관계를 이용하여 압축한다.

  • I-frame : 기준이 되는 프레임 : 하나의 온전한 이미지를 저장한다. 따라서 디코딩할 때 다른 프레임이 필요하지 않다. ( 디코딩 시간 절약 )
  • P-frame : I-frame 이후 다음 I-frame 까지의 변경된 전보만을 담는다. 비교적 적은 데이터를 저장한다. 그러나 P-frame을 디코딩하려면 연결된 I-frame이 필요하다.
  • B-frame : 다음 P 또는 I - frame에 변경된 정보만을 담고있다. 가장적은 데이터를 지니고 있으나, 디코딩 할 때 가장 많은 정보가 필요하며 디코딩 시 부하가 크다.
  • GOP(group of picture) : I-frame 과 I-frame 사이의 frame들의 수. 비디오 프레임은 GOP단위로 압축된다. 


4. 비트레이트 ( Bitrate )


멀티미디어를 코덱을 통해 인코딩 할 때는 비트레이트를 할당하게 된다. 비트레이트는 특정한 시간단위 (초) 마다 처리할 수 있는 비트의 수를 나타낸다. 인코딩 시 비트레이트를 어떻게 할당하냐에 따라 품질이 크게 달라진다.

작은해상도와 낮은 셈플링 레이트를 가진 영상을 인코딩할 떄는 많은 비트레이트가 필요하지 않다. 눈에띄는 변화가 없으며 용량만 잡아먹게 된다.

반대로 큰 해상도와 높은 샘플링 레이트를 가진 영상에 낮은 비트레이트를 할당하여 인코딩하면 화면이 뭉개지고 잡음이 샘해진다.


  • 가변 비트레이트 ( VBR, Variable Bitrate ) : 영상의 복잡도에 따라 할당하는 비트레이트의 양이 결정된다. 움직임이 많은 구간에선 압축률이 낮아짐으로 높은 비트레이트를 할당한다. 영상의 복잡도는 인코딩 중 실시간으로 판단가능하기 때문에 인코딩시간이 구배정도 더 걸릴 수 있다.
  • 고정 비트레이트 ( CBR, Constant Bitrate ) : 항상 같은양의 비트레이트를 할당한다. 품질은 상대적으로 떨어지나 일정한 인코딩 시간과, 실시간 스트리밍에 필요한 최소 대역폭을 알 수 있다.
  • 평균 비트레이트 ( ABR, Average Bitrate ) : 고정과 가변 비트레이트의 장점을 지닌다. 영상 복잡도에 따라 비트레이트를 할당하지만 평균적으로 지정된 비트레이트를 유지하려 한다. 스트리밍에도 무리가 없으며 비교적 높은 품질을 보장한다.