bink video라는 것이 있었습니다.
webm을 찾다
bink video를 대체할 포맷이 없을까 찾아보았습니다. 찾아보니 bink video도 bink hdr 이라는 후속 라이브러리도 나와있는 상태였습니다. 그러나 비용 문제가 있어, 우선 순위에서는 밀어두었습니다. 요즘 영상 주력 포맷인 h.264도 알아봤지만 이는 라이센스 관련 문제로 활용하기 힘들고, 무엇보다 알파 채널을 활용할수가 없어 기존에 사용하던 bink video를 대체할수가 없었습니다.
그러다 구글이 지원하고 있다는 Webm Project를 발견했습니다. 라이센스도 BSD 라이센스로 활용도 자유롭고, 무엇보다 알파 채널을 지원하는 것이 딱 적합했습니다. bink로 제작했던 영상 파일을 webm으로 인코딩을 해보니 VP8 코덱의 고효율 압축률 덕분에 bink로 제작했던 30여메가 용량의 영상이 1-2메가의 용량으로 줄어드는 극적인 효과를 내주었습니다.
webm에서 알파 지원다하며?
하지만 webm을 사용하기에 전혀 문제가 없는 것이 아니었습니다. 첫 번째 문제가 webm의 알파 채널 지원이 기본 기능이 아니었다는 것입니다. 무슨 말인고 하니 webm project에서 제공 하는 libvpx / libwebm 라이브러리에서는 알파 채널을 디코딩하는 기능이 없었습니다. 즉, 그냥 사용해서는 영상 내에 있는 알파 채널 정보를 얻어올수 없다는 것이죠.
일단, 사양 명세를 살펴보았습니다. webm development wiki를 살펴 보니 알파 채널 인코딩/디코딩에 관한 명세가 있었습니다. 이를 기반으로 libvpx / libwebm 소스를 분석해보니 인코딩 관련 코드를 확인할수 있었습니다. 반대로 디코딩에 관한 코드는 왜인지 없었습니다. 포럼을 검색해 봐도 알파 채널 디코딩을 공식적으로는 지원하지 않고 있다는 답변만이 있었습니다.
구글링을 해보면 알파 채널이 포함된 webm을 재생할 수 있는 크롬에 대한 관련 개발 글이 있는데, 이 글을 봐도 크롬 내에서 해당 기능을 자체적으로 개발했다는 이야기가 있습니다.
결국 해당 기능을 만들어야만 하는 것이었습니다.
만들어야지 어쩌겠어
먼저 libwebm에서 webm 인코딩시 알파 정보를 어떻게 저장하는지 분석을 하였습니다. webm은 Matroka 컨테이너 포맷을 이용해 정보를 저장합니다. cluster 안에 block group이 있고, 각 block에 frame 정보가 포함되어 있습니다. libwebm 소스를 보면 인코딩 할 때, 알파 채널 정보가 존재하는 경우 mkvmuxerutil를 이용해 block에 추가 정보(block additional)를 저장하고 있는 것을 확인할 수 있습니다.
if (frame->additional()) { if (!WriteEbmlMasterElement(writer, libwebm::kMkvBlockAdditions, block_additions_payload_size)) { return 0; } if (!WriteEbmlMasterElement(writer, libwebm::kMkvBlockMore, block_more_payload_size)) return 0; if (!WriteEbmlElement(writer, libwebm::kMkvBlockAddID, static_cast<uint64>(frame->add_id()))) return 0; if (!WriteEbmlElement(writer, libwebm::kMkvBlockAdditional, frame->additional(), frame->additional_length())) { return 0; } }
어떻게 저장하는지 알았으니 디코딩 할 때, 이걸 읽어내면 되겠습니다. mkvparser에서 block을 읽을 때, libwebm::kMkvBlockAdditions가 있으면 위의 정보를 추가적으로 읽게 수정해줍니다. 그리고 block에 추가 정보의 frame 위치와 크기를 저장하도록 수정해줍니다. 그렇게 수정 후 libwebm의 webm_info를 통해 해당 정보를 제대로 파싱하고 있는지 확인해줍니다.
frame_addition 위치와 크기가 제대로 표시되는군요.
YUVA to RGBA
// 기존 변환 코드. 모든 픽셀을 일일히 변환 하고 있다. for (unsigned long int i = 0; i < height; ++i) { for (unsigned long int j = 0; j < width; ++j) { int t_y = y[((i * strideY) + j)]; int t_u = u[(((i >> 1) * strideU) + (j >> 1))]; int t_v = v[(((i >> 1) * strideV) + (j >> 1))]; int t_a = a[((i * strideA) + j)]; t_y = t_y < 16 ? 16 : t_y; int r = (298 * (t_y - 16) + 409 * (t_v - 128) + 128) >> 8; int g = (298 * (t_y - 16) - 100 * (t_u - 128) - 208 * (t_v - 128) + 128) >> 8; int b = (298 * (t_y - 16) + 516 * (t_u - 128) + 128) >> 8; mCTX.pixels[pixels++] = r > 255 ? 255 : r < 0 ? 0 : r; mCTX.pixels[pixels++] = g > 255 ? 255 : g < 0 ? 0 : g; mCTX.pixels[pixels++] = b > 255 ? 255 : b < 0 ? 0 : b; mCTX.pixels[pixels++] = t_a; } }
for (uint32_t h = 0; h < (height - 1); h += 2) { const uint8_t *y_ptr1 = Y + h * Y_stride; const uint8_t *y_ptr2 = Y + (h + 1) * Y_stride; const uint8_t *u_ptr = U + (h / 2) * U_stride; const uint8_t *v_ptr = V + (h / 2) * V_stride; const uint8_t *a_ptr1 = (A) ? A + h * A_stride : nullptr; const uint8_t *a_ptr2 = (A) ? A + (h + 1) * A_stride : nullptr; uint8_t *rgba_ptr1 = RGBA + (h * RGBA_stride); uint8_t *rgba_ptr2 = RGBA + ((h + 1) * RGBA_stride); for (uint32_t w = 0; w < (width - 31); w += 32) { __m128i u = LOAD_SI128((const __m128i*)(u_ptr)); __m128i v = LOAD_SI128((const __m128i*)(v_ptr)); __m128i r_tmp, g_tmp, b_tmp; __m128i r_16_1, g_16_1, b_16_1, r_16_2, g_16_2, b_16_2; __m128i r_uv_16_1, g_uv_16_1, b_uv_16_1, r_uv_16_2, g_uv_16_2, b_uv_16_2; __m128i y_16_1, y_16_2; u = _mm_add_epi8(u, _mm_set1_epi8(-128)); v = _mm_add_epi8(v, _mm_set1_epi8(-128)); /* process first 16 pixels of first line */ __m128i u_16 = _mm_srai_epi16(_mm_unpacklo_epi8(u, u), 8); __m128i v_16 = _mm_srai_epi16(_mm_unpacklo_epi8(v, v), 8); UV2RGB_16(u_16, v_16, r_uv_16_1, g_uv_16_1, b_uv_16_1, r_uv_16_2, g_uv_16_2, b_uv_16_2) r_16_1 = r_uv_16_1; g_16_1 = g_uv_16_1; b_16_1 = b_uv_16_1; r_16_2 = r_uv_16_2; g_16_2 = g_uv_16_2; b_16_2 = b_uv_16_2; __m128i y = LOAD_SI128((const __m128i*)(y_ptr1)); y = _mm_sub_epi8(y, _mm_set1_epi8(param->y_offset)); y_16_1 = _mm_unpacklo_epi8(y, _mm_setzero_si128()); y_16_2 = _mm_unpackhi_epi8(y, _mm_setzero_si128()); ADD_Y2RGB_16(y_16_1, y_16_2, r_16_1, g_16_1, b_16_1, r_16_2, g_16_2, b_16_2); __m128i r_8_11 = _mm_packus_epi16(r_16_1, r_16_2); __m128i g_8_11 = _mm_packus_epi16(g_16_1, g_16_2); __m128i b_8_11 = _mm_packus_epi16(b_16_1, b_16_2); /* process first 16 pixels of second line */ r_16_1 = r_uv_16_1; g_16_1 = g_uv_16_1; b_16_1 = b_uv_16_1; r_16_2 = r_uv_16_2; g_16_2 = g_uv_16_2; b_16_2 = b_uv_16_2; y = LOAD_SI128((const __m128i*)(y_ptr2)); y = _mm_sub_epi8(y, _mm_set1_epi8(param->y_offset)); y_16_1 = _mm_unpacklo_epi8(y, _mm_setzero_si128()); y_16_2 = _mm_unpackhi_epi8(y, _mm_setzero_si128()); ADD_Y2RGB_16(y_16_1, y_16_2, r_16_1, g_16_1, b_16_1, r_16_2, g_16_2, b_16_2); __m128i r_8_21 = _mm_packus_epi16(r_16_1, r_16_2); __m128i g_8_21 = _mm_packus_epi16(g_16_1, g_16_2); __m128i b_8_21 = _mm_packus_epi16(b_16_1, b_16_2); /* process last 16 pixels of first line */ u_16 = _mm_srai_epi16(_mm_unpackhi_epi8(u, u), 8); v_16 = _mm_srai_epi16(_mm_unpackhi_epi8(v, v), 8); UV2RGB_16(u_16, v_16, r_uv_16_1, g_uv_16_1, b_uv_16_1, r_uv_16_2, g_uv_16_2, b_uv_16_2); r_16_1 = r_uv_16_1; g_16_1 = g_uv_16_1; b_16_1 = b_uv_16_1; r_16_2 = r_uv_16_2; g_16_2 = g_uv_16_2; b_16_2 = b_uv_16_2; y = LOAD_SI128((const __m128i*)(y_ptr1 + 16)); y = _mm_sub_epi8(y, _mm_set1_epi8(param->y_offset)); y_16_1 = _mm_unpacklo_epi8(y, _mm_setzero_si128()); y_16_2 = _mm_unpackhi_epi8(y, _mm_setzero_si128()); ADD_Y2RGB_16(y_16_1, y_16_2, r_16_1, g_16_1, b_16_1, r_16_2, g_16_2, b_16_2); __m128i r_8_12 = _mm_packus_epi16(r_16_1, r_16_2); __m128i g_8_12 = _mm_packus_epi16(g_16_1, g_16_2); __m128i b_8_12 = _mm_packus_epi16(b_16_1, b_16_2); /* process last 16 pixels of second line */ r_16_1 = r_uv_16_1; g_16_1 = g_uv_16_1; b_16_1 = b_uv_16_1; r_16_2 = r_uv_16_2; g_16_2 = g_uv_16_2; b_16_2 = b_uv_16_2; y = LOAD_SI128((const __m128i*)(y_ptr2 + 16)); y = _mm_sub_epi8(y, _mm_set1_epi8(param->y_offset)); y_16_1 = _mm_unpacklo_epi8(y, _mm_setzero_si128()); y_16_2 = _mm_unpackhi_epi8(y, _mm_setzero_si128()); ADD_Y2RGB_16(y_16_1, y_16_2, r_16_1, g_16_1, b_16_1, r_16_2, g_16_2, b_16_2); __m128i r_8_22 = _mm_packus_epi16(r_16_1, r_16_2); __m128i g_8_22 = _mm_packus_epi16(g_16_1, g_16_2); __m128i b_8_22 = _mm_packus_epi16(b_16_1, b_16_2); __m128i rgb[6]; PACK_RGB24_32(r_8_11, r_8_12, g_8_11, g_8_12, b_8_11, b_8_12, rgb[0], rgb[1], rgb[2], rgb[3], rgb[4], rgb[5]); uint8_t *rgb_src = rgb[0].m128i_u8; for (int i = 0; i < 32; ++i) { memcpy(rgba_ptr1, rgb_src, sizeof(uint8_t) * 3); rgba_ptr1 += 3; rgb_src += 3; *(rgba_ptr1++) = (a_ptr1) ? *(a_ptr1++) : 255; } PACK_RGB24_32(r_8_21, r_8_22, g_8_21, g_8_22, b_8_21, b_8_22, rgb[0], rgb[1], rgb[2], rgb[3], rgb[4], rgb[5]); rgb_src = rgb[0].m128i_u8; for (int i = 0; i < 32; ++i) { memcpy(rgba_ptr2, rgb_src, sizeof(uint8_t) * 3); rgba_ptr2 += 3; rgb_src += 3; *(rgba_ptr2++) = (a_ptr2) ? *(a_ptr2++) : 255; } y_ptr1 += 32; y_ptr2 += 32; u_ptr += 16; v_ptr += 16; } }
결론
소스 코드
https://github.com/KindTis/Webm2RGBA
https://github.com/KindTis/libwebm
참고
https://github.com/descampsa/yuv2rgb
https://developers.google.com/web/updates/2013/07/Alpha-transparency-in-Chrome-video
https://github.com/tomdalling/opengl-series