러스트로 MPEG-4 파일 시스템 라이브러리를 만들어보자.

Ian Jun
Spoon Radio
Published in
9 min readAug 5, 2020

MPEG-4 파일 시스템은 업계쪽 사람이 ISO BMFF(ISO Base Media File Format)라고 부르는 가장 대중적인 미디어 컨테이너입니다. 일반 사용자에게는 MP4 파일로 더 잘 알려져 있습니다. 가장 대중적인 포맷은 가장 호환이 잘 된다는 의미이기도 합니다. 미디어 파일을 만든다면 ISO BMFF는 좋은 선택입니다.

오디오팀에서 신규 개발은 러스트(Rust)나 고랭(Golang) 중에서 선택합니다. 성능과 안전함, 개발 생산성을 모두 만족하는 언어이기 때문입니다. 안타깝게도, 종종 라이브러리의 부재를 경험합니다. 여기서 소개하는 ISO BMFF도 같은 사례인데, 이 포맷을 제대로 다루는 라이브러리-러스트 용어로는 크레이트(crate)-가 없었습니다.

물론 C/C++ 라이브러리로는 존재하며 이것을 러스트로 감싼 크레이트 찾을 수 있습니다. 그런데, 이런 방식은 러스트의 안전한 특성을 온전히 보장받지 못합니다. 러스트가 해결해주는 댕글링 포인터(dangling pointer)나 데이터 레이스(data race)가 발생할 수 있습니다.

러스트가 아닌 코드는 unsafe라고 표시해야 합니다. unsafe 블럭이나 함수는 러스트의 감시 대상에서 제외됩니다. 아래는 unsafe 키워드를 사용한 샘플 코드 입니다. 예를 들면, 아래 코드는 스푼라디오의 libsrt-rs 중의 일부로 C++로 작성한 SRT 라이브러리를 러스트에서 사용하기 위해서 unsafe 블럭을 사용했습니다.

pub fn peer_addr(&self) -> io::Result<SocketAddr> {
sockname(|buf, len| unsafe {
ffi::srt_getpeername(self.0, buf, len as *mut _)
})
}
pub fn socket_addr(&self) -> io::Result<SocketAddr> {
sockname(|buf, len| unsafe {
ffi::srt_getsockname(self.0, buf, len as *mut _)
})
}
pub fn recv(&self, buf: &mut [u8]) -> io::Result<usize> {
let ret = err::cvt(unsafe {
ffi::srt_recvmsg(self.0,
buf.as_mut_ptr() as *mut c_char,
buf.len() as int)
})?;
Ok(ret as usize)
}

코드 여기저기에 unsafe 키워드를 사용한 것을 확인할 수 있습니다. unsafe 키워드는 이름 그 자체로 가능하면 피하고 싶지 않습니까?

ISO BMFF를 구현한 순수한 러스트 크레이트는 전부 인큐베이팅 수준에 불과했습니다. 미디어 데이터를 읽어서 재생할 수도 인코딩된 데이터를 저장 할 수 없었습니다. 새로운 크레이터를 만들까 고민하다가, 약간이라도 진행된 mp4rs를 선택하여 사용 가능한 수준으로 개선하기로 했습니다. 지금 생각해보면 처음부터 만들어도 될 만큼 코딩을 했습니다.

첫번째 풀리퀘스트 : https://github.com/alfg/mp4rs/pull/7

2020년 7월 24일

첫번째 작업은 소소한 리펙토링이었습니다. 동일한 행위에 대해서 다른 이름을 부여한 함수가 많았습니다. 이러한 개별 함수를 트레이트(trait)를 이용하여 같은 이름으로 변경하는 작업부터 시작했습니다.

A type’s behavior consists of the methods we can call on that type. Different types share the same behavior if we can call the same methods on all of those types. Trait definitions are a way to group method signatures together to define a set of behaviors necessary to accomplish some purpose.

두번째 풀리퀘스트 : https://github.com/alfg/mp4rs/pull/9

2020년 7월 27일

본격적인 개선을 하려고 보니 여기 저기 중복하여 코드를 넣어야 했습니다. 불필요한 중복 코딩없이 손쉽게 코드를 추가할 수 있도록 변경했습니다. 박스(*) 타입을 쉽게 생성할 수 있는 매크로와 박스의 확장 헤더를 읽는 공통 함수를 추가했습니다. 이 작업은 이후의 개발에 매우 많은 도움이 되었습니다.

(* ISO BMFF는 박스라고 하는 객체 단위로 구성되어 있습니다. BMFF가 XML에 대응한다면, 박스는 XML 노드에 해당합니다.)

세번째 풀리퀘스트 : https://github.com/alfg/mp4rs/pull/11

2020년 7월 28일

ISO BMFF 헤더를 쓰는 기능을 구현했습니다. 더불어, 읽기 쓰기 모두에 대해서 ISO BMFF의 large file 스펙을 구현했습니다. 리펙토링으로는 비대하게 큰 파일을 여러 파일로 분리하였습니다.

네번째 풀리퀘스트 : https://github.com/alfg/mp4rs/pull/12

2020년 7월 29일

ISO BMFF 헤더 읽기와 쓰기를 조합하여 테스트 코드를 작성했습니다. 이 작업 역시 이후의 개발에 매우 많은 도움이 되었습니다. 테스트 코드를 돌리는 것만으로 추가 개발 과정에서의 대부분의 실수나 버그를 해결할 수 있었습니다.

다섯번째 풀리퀘스트 : https://github.com/alfg/mp4rs/pull/13

2020년 7월 31일

H.264AAC 관련 박스를 구현했습니다. 미디어 샘플 테이블을 해석해서 비디오/오디오를 읽을 수 있도록 개선했습니다. 이제 미디어 플레이어 붙일 수 있는 상태가 되었습니다.

여섯번째 풀리퀘스트 : https://github.com/alfg/mp4rs/pull/14

2020년 8월 4일

헤더 뿐만 아니라 비디오/오디오 데이터를 저장할 수 있도록 개선했습니다. 이를 위해서 H.264와 AAC의 바이너리 구문을 해석하거나 조합하는 코드를 추가했습니다. 미디어 데이터를 저장하고, 관련 메타 정보를 일련의 테이블로 구성하는 코드를 작성했습니다. 이제 인코더의 저장 모듈로 사용할 수 있습니다.

이로써 미디어 플레이어, 인코더와 같은 미디어 프로그램에 필요한 기능을 거의 대부분 구현하였습니다. mp4rs는 러스트로 작성된 가장 완성도 있는 ISO BMFF 모듈입니다.

mp4rs/mp4copy로 생성한 파일을 ffplay로 재생하고 있다.

대략 열흘 동안 10,000 라인 이상의 코드를 추가/개선 했습니다 .어느덧 이 오픈소스의 독보적인 기여자가 되어 있었습니다.

어느덧 가장 많은 기여자가 되어 있었다.

ISO BMFF를 떠나서 개발 관점에서 공유하고 싶은 것은 것이 있습니다. 위에 개발 진행 과정에서도 언급했듯이, (1) 코드를 쉽게 추가/개선할 수 있는 구조와 (2) 테스트 코드 작성은 매우 중요합니다. 이 두가지를 잘 신경쓰면, 이후에 개발 생산성에 지대한 영향을 미칩니다.

예를 들면, 초반에 작성한 boxtype!() 매크로는 박스를 추가하는데 매우 편리했습니다.

macro_rules! boxtype {
($( $name:ident => $value:expr ),*) => {
#[derive(Clone, Copy, PartialEq)]
pub enum BoxType {
$( $name, )*
UnknownBox(u32),
}
impl From<u32> for BoxType {
fn from(t: u32) -> BoxType {
match t {
$( $value => BoxType::$name, )*
_ => BoxType::UnknownBox(t),
}
}
}
impl Into<u32> for BoxType {
fn into(self) -> u32 {
match self {
$( BoxType::$name => $value, )*
BoxType::UnknownBox(t) => t,
}
}
}
}
}
boxtype! {
FtypBox => 0x66747970,
MvhdBox => 0x6d766864,
FreeBox => 0x66726565,
MdatBox => 0x6d646174,
MoovBox => 0x6d6f6f76,
MoofBox => 0x6d6f6f66,
TkhdBox => 0x746b6864,
// ...
}

boxtype!() 매크로에

AvcCBox => 0x61766343,

한줄만 추가하면 필요한 부가적인 코드를 자동으로 생성해줍니다.

중반에 작성한 테스트 코드 역시 많은 도움을 주었습니다. 기능을 추가하고 검증을 마치면 회귀테스트를 수행하는 코드를 작성했습니다. 후반으로 갈수록 추가된 기능이 많아지는데, 기존 기능을 자동화 테스트로 쉽게 검증할 수 있었습니다. 이것을 믿고 마음껏 리펙토링과 개선 작업을 진행할 수 있었습니다.

스푼라디오의 개발진은 오픈소스를 적극적으로 활용하고 있습니다. 오픈소스로 작성된 핵심 기술을 서비스에 잘 녹여내는 것은 스푼라디오 개발자의 중요한 자질입니다. 더 나아가, 오픈소스를 사용하는데만 머물지 않고 적극 참여도 합니다. 버그를 고치고 부족한 점을 개선하여 메인테이너에게 보냅니다.

아래 목록은 이번 구현에 참고한 표준입니다. mp4rs를 분석하는데 도움이 될 것입니다.

  • ISO/IEC 14496–12:2008 : ISO Base Media File Format 전체
  • ISO/IEC 14496–15:2010 : AVC SPS/PPS NAL syntax and semantics 관련 정보
  • ISO/IEC 14496–10:2012 : AVC Video Stream Definition, AVC Decoder Configuration Record 관련 정보
  • ISO/IEC 14496–1:2001 : ES Descriptor, Decoder Configuration Descriptor, SL Packet Header Configuration 관련 정보
  • ISO/IEC 14496–3:2009 : Audio Specific Config 관련 정보

--

--