From f738736fe2f2d2ae2f0abefca78defd7725af499 Mon Sep 17 00:00:00 2001 From: Jakub Pryc <94321002+Noarkhh@users.noreply.github.com> Date: Mon, 8 Jul 2024 10:16:41 +0200 Subject: [PATCH] Create encoder (#2) --- bundlex.exs | 21 ++- c_src/membrane_vpx_plugin/vpx_common.c | 85 ++++++++++ c_src/membrane_vpx_plugin/vpx_common.h | 28 ++++ c_src/membrane_vpx_plugin/vpx_decoder.c | 65 ++----- c_src/membrane_vpx_plugin/vpx_decoder.h | 6 +- .../membrane_vpx_plugin/vpx_decoder.spec.exs | 2 +- c_src/membrane_vpx_plugin/vpx_encoder.c | 158 ++++++++++++++++++ c_src/membrane_vpx_plugin/vpx_encoder.h | 14 ++ .../membrane_vpx_plugin/vpx_encoder.spec.exs | 26 +++ lib/membrane_vpx/decoder/vp8_decoder.ex | 6 +- lib/membrane_vpx/decoder/vp9_decoder.ex | 6 +- lib/membrane_vpx/decoder/vpx_decoder.ex | 18 +- lib/membrane_vpx/encoder/vp8_encoder.ex | 41 +++++ lib/membrane_vpx/encoder/vp9_encoder.ex | 41 +++++ lib/membrane_vpx/encoder/vpx_encoder.ex | 124 ++++++++++++++ .../encoder/vpx_encoder_native.ex | 18 ++ mix.exs | 9 +- mix.lock | 17 +- test/fixtures/ref_vp8.ivf | Bin 0 -> 32634 bytes test/fixtures/ref_vp9.ivf | Bin 0 -> 22218 bytes test/membrane_vpx_plugin/vpx_decoder_test.exs | 6 +- test/membrane_vpx_plugin/vpx_encoder_test.exs | 61 +++++++ 22 files changed, 655 insertions(+), 97 deletions(-) create mode 100644 c_src/membrane_vpx_plugin/vpx_common.c create mode 100644 c_src/membrane_vpx_plugin/vpx_common.h create mode 100644 c_src/membrane_vpx_plugin/vpx_encoder.c create mode 100644 c_src/membrane_vpx_plugin/vpx_encoder.h create mode 100644 c_src/membrane_vpx_plugin/vpx_encoder.spec.exs create mode 100644 lib/membrane_vpx/encoder/vp8_encoder.ex create mode 100644 lib/membrane_vpx/encoder/vp9_encoder.ex create mode 100644 lib/membrane_vpx/encoder/vpx_encoder.ex create mode 100644 lib/membrane_vpx/encoder/vpx_encoder_native.ex create mode 100644 test/fixtures/ref_vp8.ivf create mode 100644 test/fixtures/ref_vp9.ivf create mode 100644 test/membrane_vpx_plugin/vpx_encoder_test.exs diff --git a/bundlex.exs b/bundlex.exs index e7d8486..bdac040 100644 --- a/bundlex.exs +++ b/bundlex.exs @@ -11,7 +11,18 @@ defmodule Membrane.VPx.BundlexProject do [ vpx_decoder: [ interface: :nif, - sources: ["vpx_decoder.c"], + sources: ["vpx_decoder.c", "vpx_common.c"], + os_deps: [ + libvpx: [ + {:precompiled, Membrane.PrecompiledDependencyProvider.get_dependency_url(:libvpx)}, + {:pkg_config, "vpx"} + ] + ], + preprocessor: Unifex + ], + vpx_encoder: [ + interface: :nif, + sources: ["vpx_encoder.c", "vpx_common.c"], os_deps: [ libvpx: [ {:precompiled, Membrane.PrecompiledDependencyProvider.get_dependency_url(:libvpx)}, @@ -20,14 +31,6 @@ defmodule Membrane.VPx.BundlexProject do ], preprocessor: Unifex ] - # vpx_encoder: [ - # interface: :nif, - # sources: ["vpx_encoder.c"], - # os_deps: [ - # libvpx: [{:pkg_config, "vpx"}] - # ], - # preprocessor: Unifex - # ] ] end end diff --git a/c_src/membrane_vpx_plugin/vpx_common.c b/c_src/membrane_vpx_plugin/vpx_common.c new file mode 100644 index 0000000..d9efffe --- /dev/null +++ b/c_src/membrane_vpx_plugin/vpx_common.c @@ -0,0 +1,85 @@ +#include "vpx_common.h" + +UNIFEX_TERM result_error( + UnifexEnv *env, + const char *reason, + UNIFEX_TERM (*result_error_fun)(UnifexEnv *, const char *), + vpx_codec_ctx_t *codec_context, + void *state +) { + char *full_reason; + if (codec_context) { + const char *error = vpx_codec_error(codec_context); + const char *detail = vpx_codec_error_detail(codec_context); + if (detail) { + full_reason = unifex_alloc(strlen(reason) + strlen(error) + strlen(detail) + 5); + sprintf(full_reason, "%s: %s: %s", reason, error, detail); + } else { + full_reason = unifex_alloc(strlen(reason) + strlen(error) + 3); + sprintf(full_reason, "%s: %s", reason, error); + } + } else { + full_reason = unifex_alloc(strlen(reason) + 1); + sprintf(full_reason, "%s", reason); + } + + if (state) unifex_release_resource(state); + + UNIFEX_TERM result = result_error_fun(env, full_reason); + unifex_free(full_reason); + return result; +} + +Dimensions get_plane_dimensions(const vpx_image_t *img, int plane) { + const int height = + (plane > 0 && img->y_chroma_shift > 0) ? (img->d_h + 1) >> img->y_chroma_shift : img->d_h; + + int width = + (plane > 0 && img->x_chroma_shift > 0) ? (img->d_w + 1) >> img->x_chroma_shift : img->d_w; + + // Fixing NV12 chroma width if it is odd + if (img->fmt == VPX_IMG_FMT_NV12 && plane == 1) width = (width + 1) & ~1; + + return (Dimensions){width, height}; +} + +void convert_between_image_and_raw_frame( + vpx_image_t *img, UnifexPayload *raw_frame, ConversionType conversion_type +) { + const int bytes_per_pixel = (img->fmt & VPX_IMG_FMT_HIGHBITDEPTH) ? 2 : 1; + + // Assuming that for nv12 we write all chroma data at once + const int number_of_planes = (img->fmt == VPX_IMG_FMT_NV12) ? 2 : 3; + unsigned char *frame_data = raw_frame->data; + + for (int plane = 0; plane < number_of_planes; ++plane) { + unsigned char *image_buf = img->planes[plane]; + const int stride = img->stride[plane]; + Dimensions plane_dimensions = get_plane_dimensions(img, plane); + + for (unsigned int y = 0; y < plane_dimensions.height; ++y) { + size_t bytes_to_write = bytes_per_pixel * plane_dimensions.width; + switch (conversion_type) { + case RAW_FRAME_TO_IMAGE: + memcpy(image_buf, frame_data, bytes_to_write); + break; + + case IMAGE_TO_RAW_FRAME: + memcpy(frame_data, image_buf, bytes_to_write); + break; + } + image_buf += stride; + frame_data += bytes_to_write; + } + } +} + +void free_payloads(UnifexPayload **payloads, unsigned int payloads_cnt) { + for (unsigned int i = 0; i < payloads_cnt; i++) { + if (payloads[i] != NULL) { + unifex_payload_release(payloads[i]); + unifex_free(payloads[i]); + } + } + unifex_free(payloads); +} diff --git a/c_src/membrane_vpx_plugin/vpx_common.h b/c_src/membrane_vpx_plugin/vpx_common.h new file mode 100644 index 0000000..0d492ea --- /dev/null +++ b/c_src/membrane_vpx_plugin/vpx_common.h @@ -0,0 +1,28 @@ +#pragma once +#include "vpx/vpx_codec.h" +#include "vpx/vpx_image.h" +#include +#include + +typedef struct Dimensions { + unsigned int width; + unsigned int height; +} Dimensions; + +UNIFEX_TERM result_error( + UnifexEnv *env, + const char *reason, + UNIFEX_TERM (*result_error_fun)(UnifexEnv *, const char *), + vpx_codec_ctx_t *codec_context, + void *state +); + +typedef enum ConversionType { IMAGE_TO_RAW_FRAME, RAW_FRAME_TO_IMAGE } ConversionType; + +Dimensions get_plane_dimensions(const vpx_image_t *img, int plane); + +void free_payloads(UnifexPayload **payloads, unsigned int payloads_cnt); + +void convert_between_image_and_raw_frame( + vpx_image_t *img, UnifexPayload *raw_frame, ConversionType conversion_type +); \ No newline at end of file diff --git a/c_src/membrane_vpx_plugin/vpx_decoder.c b/c_src/membrane_vpx_plugin/vpx_decoder.c index 59bfc4c..06db615 100644 --- a/c_src/membrane_vpx_plugin/vpx_decoder.c +++ b/c_src/membrane_vpx_plugin/vpx_decoder.c @@ -1,5 +1,8 @@ #include "vpx_decoder.h" +// The following code is based on the simple_decoder example provided by libvpx +// (https://github.com/webmproject/libvpx/blob/main/examples/simple_decoder.c) + void handle_destroy_state(UnifexEnv *env, State *state) { UNIFEX_UNUSED(env); @@ -20,28 +23,13 @@ UNIFEX_TERM create(UnifexEnv *env, Codec codec) { } if (vpx_codec_dec_init(&state->codec_context, state->codec_interface, NULL, 0)) { - result = create_result_error(env, "Failed to initialize decoder"); - unifex_release_state(env, state); - return result; + return result_error(env, "Failed to initialize decoder", create_result_error, NULL, state); } result = create_result_ok(env, state); unifex_release_state(env, state); return result; } -Dimensions get_plane_dimensions(const vpx_image_t *img, int plane) { - const int height = - (plane > 0 && img->y_chroma_shift > 0) ? (img->d_h + 1) >> img->y_chroma_shift : img->d_h; - - int width = - (plane > 0 && img->x_chroma_shift > 0) ? (img->d_w + 1) >> img->x_chroma_shift : img->d_w; - - // Fixing NV12 chroma width if it is odd - if (img->fmt == VPX_IMG_FMT_NV12 && plane == 1) - width = (width + 1) & ~1; - - return (Dimensions){width, height}; -} size_t get_image_byte_size(const vpx_image_t *img) { const int bytes_per_pixel = (img->fmt & VPX_IMG_FMT_HIGHBITDEPTH) ? 2 : 1; const int number_of_planes = (img->fmt == VPX_IMG_FMT_NV12) ? 2 : 3; @@ -55,25 +43,8 @@ size_t get_image_byte_size(const vpx_image_t *img) { return image_size; } -void get_output_frame_from_image(const vpx_image_t *img, UnifexPayload *output_frame) { - const int bytes_per_pixel = (img->fmt & VPX_IMG_FMT_HIGHBITDEPTH) ? 2 : 1; - - // Assuming that for nv12 we write all chroma data at once - const int number_of_planes = (img->fmt == VPX_IMG_FMT_NV12) ? 2 : 3; - unsigned char *frame_data = output_frame->data; - - for (int plane = 0; plane < number_of_planes; ++plane) { - const unsigned char *buf = img->planes[plane]; - const int stride = img->stride[plane]; - Dimensions plane_dimensions = get_plane_dimensions(img, plane); - - for (unsigned int y = 0; y < plane_dimensions.height; ++y) { - size_t bytes_to_write = bytes_per_pixel * plane_dimensions.width; - memcpy(frame_data, buf, bytes_to_write); - buf += stride; - frame_data += bytes_to_write; - } - } +void get_raw_frame_from_image(vpx_image_t *img, UnifexPayload *raw_frame) { + convert_between_image_and_raw_frame(img, raw_frame, IMAGE_TO_RAW_FRAME); } void alloc_output_frame(UnifexEnv *env, const vpx_image_t *img, UnifexPayload **output_frame) { @@ -85,19 +56,14 @@ PixelFormat get_pixel_format_from_image(vpx_image_t *img) { switch (img->fmt) { case VPX_IMG_FMT_I422: return PIXEL_FORMAT_I422; - case VPX_IMG_FMT_I420: return PIXEL_FORMAT_I420; - case VPX_IMG_FMT_I444: return PIXEL_FORMAT_I444; - case VPX_IMG_FMT_YV12: return PIXEL_FORMAT_YV12; - case VPX_IMG_FMT_NV12: return PIXEL_FORMAT_NV12; - default: return PIXEL_FORMAT_I420; } @@ -107,11 +73,13 @@ UNIFEX_TERM decode_frame(UnifexEnv *env, UnifexPayload *frame, State *state) { vpx_codec_iter_t iter = NULL; vpx_image_t *img = NULL; PixelFormat pixel_format = PIXEL_FORMAT_I420; - unsigned int frames_cnt = 0, allocated_frames = 2; - UnifexPayload **output_frames = unifex_alloc(allocated_frames * sizeof(*output_frames)); + unsigned int frames_cnt = 0, allocated_frames = 1; + UnifexPayload **output_frames = unifex_alloc(allocated_frames * sizeof(UnifexPayload*)); if (vpx_codec_decode(&state->codec_context, frame->data, frame->size, NULL, 0)) { - return decode_frame_result_error(env, "Decoding frame failed"); + return result_error( + env, "Decoding frame failed", decode_frame_result_error, &state->codec_context, NULL + ); } while ((img = vpx_codec_get_frame(&state->codec_context, &iter)) != NULL) { @@ -121,19 +89,14 @@ UNIFEX_TERM decode_frame(UnifexEnv *env, UnifexPayload *frame, State *state) { } alloc_output_frame(env, img, &output_frames[frames_cnt]); - get_output_frame_from_image(img, output_frames[frames_cnt]); + get_raw_frame_from_image(img, output_frames[frames_cnt]); pixel_format = get_pixel_format_from_image(img); frames_cnt++; } UNIFEX_TERM result = decode_frame_result_ok(env, output_frames, frames_cnt, pixel_format); - for (unsigned int i = 0; i < frames_cnt; i++) { - if (output_frames[i] != NULL) { - unifex_payload_release(output_frames[i]); - unifex_free(output_frames[i]); - } - } - unifex_free(output_frames); + + free_payloads(output_frames, frames_cnt); return result; } diff --git a/c_src/membrane_vpx_plugin/vpx_decoder.h b/c_src/membrane_vpx_plugin/vpx_decoder.h index e743aa5..5eced85 100644 --- a/c_src/membrane_vpx_plugin/vpx_decoder.h +++ b/c_src/membrane_vpx_plugin/vpx_decoder.h @@ -1,6 +1,7 @@ #pragma once #include "vpx/vp8dx.h" #include "vpx/vpx_decoder.h" +#include "vpx_common.h" #include typedef struct State { @@ -8,9 +9,4 @@ typedef struct State { vpx_codec_iface_t *codec_interface; } State; -typedef struct Dimensions { - unsigned int width; - unsigned int height; -} Dimensions; - #include "_generated/vpx_decoder.h" \ No newline at end of file diff --git a/c_src/membrane_vpx_plugin/vpx_decoder.spec.exs b/c_src/membrane_vpx_plugin/vpx_decoder.spec.exs index c1a02f3..0e1ec06 100644 --- a/c_src/membrane_vpx_plugin/vpx_decoder.spec.exs +++ b/c_src/membrane_vpx_plugin/vpx_decoder.spec.exs @@ -12,4 +12,4 @@ spec decode_frame(payload, state) :: {:ok :: label, frames :: [payload], pixel_format :: pixel_format} | {:error :: label, reason :: atom} -dirty :cpu, create: 1, decode_frame: 2 +dirty :cpu, [:create, :decode_frame] diff --git a/c_src/membrane_vpx_plugin/vpx_encoder.c b/c_src/membrane_vpx_plugin/vpx_encoder.c new file mode 100644 index 0000000..942c0b8 --- /dev/null +++ b/c_src/membrane_vpx_plugin/vpx_encoder.c @@ -0,0 +1,158 @@ +#include "vpx_encoder.h" + +// The following code is based on the simple_encoder example provided by libvpx +// (https://github.com/webmproject/libvpx/blob/main/examples/simple_encoder.c) + +void handle_destroy_state(UnifexEnv *env, State *state) { + UNIFEX_UNUSED(env); + + vpx_codec_destroy(&state->codec_context); +} + +vpx_img_fmt_t translate_pixel_format(PixelFormat pixel_format) { + switch (pixel_format) { + case PIXEL_FORMAT_I420: + return VPX_IMG_FMT_I420; + case PIXEL_FORMAT_I422: + return VPX_IMG_FMT_I422; + case PIXEL_FORMAT_I444: + return VPX_IMG_FMT_I444; + case PIXEL_FORMAT_YV12: + return VPX_IMG_FMT_YV12; + case PIXEL_FORMAT_NV12: + return VPX_IMG_FMT_NV12; + default: + return VPX_IMG_FMT_I420; + } +} + +UNIFEX_TERM create( + UnifexEnv *env, + Codec codec, + unsigned int width, + unsigned int height, + PixelFormat pixel_format, + unsigned int encoding_deadline +) { + UNIFEX_TERM result; + State *state = unifex_alloc_state(env); + vpx_codec_enc_cfg_t config; + + switch (codec) { + case CODEC_VP8: + state->codec_interface = vpx_codec_vp8_cx(); + break; + case CODEC_VP9: + state->codec_interface = vpx_codec_vp9_cx(); + break; + } + state->encoding_deadline = encoding_deadline; + + if (vpx_codec_enc_config_default(state->codec_interface, &config, 0)) { + return result_error( + env, "Failed to get default codec config", create_result_error, NULL, state + ); + } + + config.g_h = height; + config.g_w = width; + config.g_timebase.num = 1; + config.g_timebase.den = 1000000000; // 1e9 + config.g_error_resilient = 1; + + if (vpx_codec_enc_init(&state->codec_context, state->codec_interface, &config, 0)) { + return result_error(env, "Failed to initialize encoder", create_result_error, NULL, state); + } + if (!vpx_img_alloc(&state->img, translate_pixel_format(pixel_format), width, height, 1)) { + return result_error( + env, "Failed to allocate image", create_result_error, &state->codec_context, state + ); + } + result = create_result_ok(env, state); + unifex_release_state(env, state); + return result; +} + +void get_image_from_raw_frame(vpx_image_t *img, UnifexPayload *raw_frame) { + convert_between_image_and_raw_frame(img, raw_frame, RAW_FRAME_TO_IMAGE); +} + +void alloc_output_frame( + UnifexEnv *env, const vpx_codec_cx_pkt_t *packet, UnifexPayload **output_frame +) { + *output_frame = unifex_alloc(sizeof(UnifexPayload)); + unifex_payload_alloc(env, UNIFEX_PAYLOAD_BINARY, packet->data.frame.sz, *output_frame); +} + +UNIFEX_TERM encode(UnifexEnv *env, vpx_image_t *img, vpx_codec_pts_t pts, State *state) { + vpx_codec_iter_t iter = NULL; + int flushing = (img == NULL), got_packets = 0; + const vpx_codec_cx_pkt_t *packet = NULL; + + unsigned int frames_cnt = 0, allocated_frames = 1; + UnifexPayload **encoded_frames = unifex_alloc(allocated_frames * sizeof(UnifexPayload*)); + vpx_codec_pts_t *encoded_frames_timestamps = + unifex_alloc(allocated_frames * sizeof(vpx_codec_pts_t)); + + do { + // Reasoning for the do-while and while loops comes from the description of vpx_codec_encode: + // + // When the last frame has been passed to the encoder, this function should continue to be + // called, with the img parameter set to NULL. This will signal the end-of-stream condition to + // the encoder and allow it to encode any held buffers. Encoding is complete when + // vpx_codec_encode() is called and vpx_codec_get_cx_data() returns no data. + if (vpx_codec_encode(&state->codec_context, img, pts, 1, 0, state->encoding_deadline) != + VPX_CODEC_OK) { + if (flushing) { + return result_error( + env, "Encoding frame failed", flush_result_error, &state->codec_context, NULL + ); + } else { + return result_error( + env, "Encoding frame failed", encode_frame_result_error, &state->codec_context, NULL + ); + } + } + got_packets = 0; + + while ((packet = vpx_codec_get_cx_data(&state->codec_context, &iter)) != NULL) { + got_packets = 1; + if (packet->kind != VPX_CODEC_CX_FRAME_PKT) continue; + + if (frames_cnt >= allocated_frames) { + allocated_frames *= 2; + encoded_frames = unifex_realloc(encoded_frames, allocated_frames * sizeof(*encoded_frames)); + + encoded_frames_timestamps = unifex_realloc( + encoded_frames_timestamps, allocated_frames * sizeof(*encoded_frames_timestamps) + ); + } + alloc_output_frame(env, packet, &encoded_frames[frames_cnt]); + memcpy(encoded_frames[frames_cnt]->data, packet->data.frame.buf, packet->data.frame.sz); + encoded_frames_timestamps[frames_cnt] = packet->data.frame.pts; + frames_cnt++; + } + } while (got_packets && flushing); + + UNIFEX_TERM result; + if (flushing) { + result = + flush_result_ok(env, encoded_frames, frames_cnt, encoded_frames_timestamps, frames_cnt); + } else { + result = encode_frame_result_ok( + env, encoded_frames, frames_cnt, encoded_frames_timestamps, frames_cnt + ); + } + free_payloads(encoded_frames, frames_cnt); + + return result; +} + +UNIFEX_TERM encode_frame( + UnifexEnv *env, UnifexPayload *raw_frame, vpx_codec_pts_t pts, State *state +) { + get_image_from_raw_frame(&state->img, raw_frame); + return encode(env, &state->img, pts, state); +} + +UNIFEX_TERM flush(UnifexEnv *env, State *state) { return encode(env, NULL, 0, state); } diff --git a/c_src/membrane_vpx_plugin/vpx_encoder.h b/c_src/membrane_vpx_plugin/vpx_encoder.h new file mode 100644 index 0000000..7618beb --- /dev/null +++ b/c_src/membrane_vpx_plugin/vpx_encoder.h @@ -0,0 +1,14 @@ +#pragma once +#include "vpx/vp8cx.h" +#include "vpx/vpx_encoder.h" +#include "vpx_common.h" +#include + +typedef struct State { + vpx_codec_ctx_t codec_context; + vpx_codec_iface_t *codec_interface; + vpx_image_t img; + unsigned int encoding_deadline; +} State; + +#include "_generated/vpx_encoder.h" diff --git a/c_src/membrane_vpx_plugin/vpx_encoder.spec.exs b/c_src/membrane_vpx_plugin/vpx_encoder.spec.exs new file mode 100644 index 0000000..4283c92 --- /dev/null +++ b/c_src/membrane_vpx_plugin/vpx_encoder.spec.exs @@ -0,0 +1,26 @@ +module Membrane.VPx.Encoder.Native + +state_type "State" + +type codec :: :vp8 | :vp9 + +type pixel_format :: :I420 | :I422 | :I444 | :NV12 | :YV12 + +spec create( + codec, + width :: unsigned, + height :: unsigned, + pixel_format, + encoding_deadline :: unsigned + ) :: + {:ok :: label, state} | {:error :: label, reason :: atom} + +spec encode_frame(payload, pts :: int64, state) :: + {:ok :: label, frames :: [payload], timestamps :: [int64]} + | {:error :: label, reason :: atom} + +spec flush(state) :: + {:ok :: label, frames :: [payload], timestamps :: [int64]} + | {:error :: label, reason :: atom} + +dirty :cpu, [:create, :encode_frame, :flush] diff --git a/lib/membrane_vpx/decoder/vp8_decoder.ex b/lib/membrane_vpx/decoder/vp8_decoder.ex index 897beb5..8736234 100644 --- a/lib/membrane_vpx/decoder/vp8_decoder.ex +++ b/lib/membrane_vpx/decoder/vp8_decoder.ex @@ -7,14 +7,14 @@ defmodule Membrane.VP8.Decoder do alias Membrane.{VP8, VPx} def_options width: [ - spec: non_neg_integer() | nil, + spec: pos_integer() | nil, default: nil, description: """ Width of a frame, needed if not provided with stream format. If it's not specified either in this option or the stream format, the element will crash. """ ], height: [ - spec: non_neg_integer() | nil, + spec: pos_integer() | nil, default: nil, description: """ Height of a frame, needed if not provided with stream format. If it's not specified either in this option or the stream format, the element will crash. @@ -24,7 +24,7 @@ defmodule Membrane.VP8.Decoder do spec: {non_neg_integer(), pos_integer()} | nil, default: nil, description: """ - Framerate, needed if not provided with stream format. If it's not specified either in this option or the stream format, the element will crash. + Framerate of the stream. """ ] diff --git a/lib/membrane_vpx/decoder/vp9_decoder.ex b/lib/membrane_vpx/decoder/vp9_decoder.ex index 08eae14..19663da 100644 --- a/lib/membrane_vpx/decoder/vp9_decoder.ex +++ b/lib/membrane_vpx/decoder/vp9_decoder.ex @@ -7,14 +7,14 @@ defmodule Membrane.VP9.Decoder do alias Membrane.{VP9, VPx} def_options width: [ - spec: non_neg_integer() | nil, + spec: pos_integer() | nil, default: nil, description: """ Width of a frame, needed if not provided with stream format. If it's not specified either in this option or the stream format, the element will crash. """ ], height: [ - spec: non_neg_integer() | nil, + spec: pos_integer() | nil, default: nil, description: """ Height of a frame, needed if not provided with stream format. If it's not specified either in this option or the stream format, the element will crash. @@ -24,7 +24,7 @@ defmodule Membrane.VP9.Decoder do spec: {non_neg_integer(), pos_integer()} | nil, default: nil, description: """ - Framerate, needed if not provided with stream format. If it's not specified either in this option or the stream format, the element will crash. + Framerate of the stream. """ ] diff --git a/lib/membrane_vpx/decoder/vpx_decoder.ex b/lib/membrane_vpx/decoder/vpx_decoder.ex index b2f1058..c037c55 100644 --- a/lib/membrane_vpx/decoder/vpx_decoder.ex +++ b/lib/membrane_vpx/decoder/vpx_decoder.ex @@ -10,9 +10,9 @@ defmodule Membrane.VPx.Decoder do @type t :: %__MODULE__{ codec: :vp8 | :vp9, - width: non_neg_integer() | nil, - height: non_neg_integer() | nil, - framerate: {non_neg_integer(), pos_integer()} | nil, + width: pos_integer() | nil, + height: pos_integer() | nil, + framerate: {pos_integer(), pos_integer()} | nil, decoder_ref: reference() | nil } @@ -51,7 +51,7 @@ defmodule Membrane.VPx.Decoder do @spec handle_buffer(:input, Membrane.Buffer.t(), CallbackContext.t(), State.t()) :: callback_return() def handle_buffer(:input, %Buffer{payload: payload, pts: pts}, ctx, state) do - {:ok, decoded_frames, pixel_format} = Native.decode_frame(payload, state.decoder_ref) + {:ok, [decoded_frame], pixel_format} = Native.decode_frame(payload, state.decoder_ref) stream_format_action = if ctx.pads.output.stream_format == nil do @@ -63,8 +63,8 @@ defmodule Membrane.VPx.Decoder do [] end - buffers = Enum.map(decoded_frames, &%Buffer{payload: &1, pts: pts}) - {stream_format_action ++ [buffer: {:output, buffers}], state} + {stream_format_action ++ [buffer: {:output, %Buffer{payload: decoded_frame, pts: pts}}], + state} end @spec get_output_stream_format( @@ -79,14 +79,14 @@ defmodule Membrane.VPx.Decoder do { state.width || raise("Width not provided"), state.height || raise("Height not provided"), - state.framerate || raise("Framerate not provided") + state.framerate } - %{width: width, height: height, framerate: framerate} -> + %{width: width, height: height} -> { width, height, - framerate || state.framerate || raise("Framerate not provided") + state.framerate } end diff --git a/lib/membrane_vpx/encoder/vp8_encoder.ex b/lib/membrane_vpx/encoder/vp8_encoder.ex new file mode 100644 index 0000000..aa0c27b --- /dev/null +++ b/lib/membrane_vpx/encoder/vp8_encoder.ex @@ -0,0 +1,41 @@ +defmodule Membrane.VP8.Encoder do + @moduledoc """ + Element that encodes a VP8 stream + """ + use Membrane.Filter + + alias Membrane.{VP8, VPx} + + def_options encoding_deadline: [ + spec: Membrane.Time.t() | :auto, + default: :auto, + description: """ + Determines how long should it take the encoder to encode a frame. + The longer the encoding takes the better the quality will be. If set to 0 the + encoder will take as long as it needs to produce the best frame possible. Note that + this is a soft limit, there is no guarantee that the encoding process will never exceed it. + If set to `:auto` the deadline will be calculated based on the framerate provided by + incoming stream format. If the framerate is `nil` a fixed deadline of 10ms will be set. + """ + ] + + def_input_pad :input, + accepted_format: Membrane.RawVideo + + def_output_pad :output, + accepted_format: VP8 + + @impl true + def handle_init(ctx, opts) do + VPx.Encoder.handle_init(ctx, opts, :vp8) + end + + @impl true + defdelegate handle_stream_format(pad, stream_format, ctx, state), to: VPx.Encoder + + @impl true + defdelegate handle_buffer(pad, buffer, ctx, state), to: VPx.Encoder + + @impl true + defdelegate handle_end_of_stream(pad, ctx, state), to: VPx.Encoder +end diff --git a/lib/membrane_vpx/encoder/vp9_encoder.ex b/lib/membrane_vpx/encoder/vp9_encoder.ex new file mode 100644 index 0000000..5e1c85d --- /dev/null +++ b/lib/membrane_vpx/encoder/vp9_encoder.ex @@ -0,0 +1,41 @@ +defmodule Membrane.VP9.Encoder do + @moduledoc """ + Element that encodes a VP9 stream + """ + use Membrane.Filter + + alias Membrane.{VP9, VPx} + + def_options encoding_deadline: [ + spec: Membrane.Time.t() | :auto, + default: :auto, + description: """ + Determines how long should it take the encoder to encode a frame. + The longer the encoding takes the better the quality will be. If set to 0 the + encoder will take as long as it needs to produce the best frame possible. Note that + this is a soft limit, there is no guarantee that the encoding process will never exceed it. + If set to `:auto` the deadline will be calculated based on the framerate provided by + incoming stream format. If the framerate is `nil` a fixed deadline of 10ms will be set. + """ + ] + + def_input_pad :input, + accepted_format: Membrane.RawVideo + + def_output_pad :output, + accepted_format: VP9 + + @impl true + def handle_init(ctx, opts) do + VPx.Encoder.handle_init(ctx, opts, :vp9) + end + + @impl true + defdelegate handle_stream_format(pad, stream_format, ctx, state), to: VPx.Encoder + + @impl true + defdelegate handle_buffer(pad, buffer, ctx, state), to: VPx.Encoder + + @impl true + defdelegate handle_end_of_stream(pad, ctx, state), to: VPx.Encoder +end diff --git a/lib/membrane_vpx/encoder/vpx_encoder.ex b/lib/membrane_vpx/encoder/vpx_encoder.ex new file mode 100644 index 0000000..8eb96c5 --- /dev/null +++ b/lib/membrane_vpx/encoder/vpx_encoder.ex @@ -0,0 +1,124 @@ +defmodule Membrane.VPx.Encoder do + @moduledoc false + + alias Membrane.{Buffer, RawVideo, VP8, VP9} + alias Membrane.Element.CallbackContext + alias Membrane.VPx.Encoder.Native + + @default_encoding_deadline Membrane.Time.milliseconds(10) + + defmodule State do + @moduledoc false + + @type t :: %__MODULE__{ + codec: :vp8 | :vp9, + codec_module: VP8 | VP9, + encoding_deadline: non_neg_integer(), + encoder_ref: reference() | nil + } + + @enforce_keys [:codec, :codec_module, :encoding_deadline] + defstruct @enforce_keys ++ + [ + encoder_ref: nil + ] + end + + @type callback_return :: {[Membrane.Element.Action.t()], State.t()} + + @spec handle_init(CallbackContext.t(), VP8.Encoder.t() | VP9.Encoder.t(), :vp8 | :vp9) :: + callback_return() + def handle_init(_ctx, opts, codec) do + state = %State{ + codec: codec, + codec_module: + case codec do + :vp8 -> VP8 + :vp9 -> VP9 + end, + encoding_deadline: opts.encoding_deadline + } + + {[], state} + end + + @spec handle_stream_format(:input, RawVideo.t(), CallbackContext.t(), State.t()) :: + callback_return() + def handle_stream_format(:input, stream_format, ctx, state) do + %RawVideo{ + width: width, + height: height, + framerate: framerate + } = stream_format + + output_stream_format = + struct(state.codec_module, width: width, height: height, framerate: framerate) + + {flushed_buffers, encoder_ref} = + maybe_recreate_encoder(ctx.pads.input.stream_format, stream_format, state) + + { + [buffer: {:output, flushed_buffers}, stream_format: {:output, output_stream_format}], + %{state | encoder_ref: encoder_ref} + } + end + + @spec handle_buffer(:input, Membrane.Buffer.t(), CallbackContext.t(), State.t()) :: + callback_return() + def handle_buffer(:input, %Buffer{payload: payload, pts: pts}, _ctx, state) do + {:ok, encoded_frames, timestamps} = Native.encode_frame(payload, pts, state.encoder_ref) + + buffers = + Enum.zip(encoded_frames, timestamps) + |> Enum.map(fn {frame, frame_pts} -> %Buffer{payload: frame, pts: frame_pts} end) + + {[buffer: {:output, buffers}], state} + end + + @spec handle_end_of_stream(:input, CallbackContext.t(), State.t()) :: callback_return() + def handle_end_of_stream(:input, _ctx, state) do + buffers = flush(state.encoder_ref) + {[buffer: {:output, buffers}, end_of_stream: :output], state} + end + + @spec maybe_recreate_encoder( + previous_stream_format :: RawVideo.t(), + new_stream_format :: RawVideo.t(), + State.t() + ) :: {flushed_buffers :: [Buffer.t()], encoder_ref :: reference()} + defp maybe_recreate_encoder(unchanged_stream_format, unchanged_stream_format, state) do + {[], state.encoder_ref} + end + + defp maybe_recreate_encoder(_previous_stream_format, new_stream_format, state) do + %RawVideo{ + width: width, + height: height, + framerate: framerate, + pixel_format: pixel_format + } = new_stream_format + + encoding_deadline = + case {state.encoding_deadline, framerate} do + {:auto, nil} -> @default_encoding_deadline |> Membrane.Time.as_microseconds(:round) + {:auto, {num, denom}} -> div(denom * 1_000_000, num) + {fixed_deadline, _framerate} -> fixed_deadline |> Membrane.Time.as_microseconds(:round) + end + + new_encoder_ref = + Native.create!(state.codec, width, height, pixel_format, encoding_deadline) + + case state.encoder_ref do + nil -> {[], new_encoder_ref} + old_encoder_ref -> {flush(old_encoder_ref), new_encoder_ref} + end + end + + @spec flush(reference()) :: [Membrane.Buffer.t()] + defp flush(encoder_ref) do + {:ok, encoded_frames, timestamps} = Native.flush(encoder_ref) + + Enum.zip(encoded_frames, timestamps) + |> Enum.map(fn {frame, frame_pts} -> %Buffer{payload: frame, pts: frame_pts} end) + end +end diff --git a/lib/membrane_vpx/encoder/vpx_encoder_native.ex b/lib/membrane_vpx/encoder/vpx_encoder_native.ex new file mode 100644 index 0000000..6025da7 --- /dev/null +++ b/lib/membrane_vpx/encoder/vpx_encoder_native.ex @@ -0,0 +1,18 @@ +defmodule Membrane.VPx.Encoder.Native do + @moduledoc false + use Unifex.Loader + + @spec create!( + :vp8 | :vp9, + pos_integer(), + pos_integer(), + Membrane.RawVideo.pixel_format(), + non_neg_integer() + ) :: reference() + def create!(codec, width, height, pixel_format, encoding_deadline) do + case create(codec, width, height, pixel_format, encoding_deadline) do + {:ok, decoder_ref} -> decoder_ref + {:error, reason} -> raise "Failed to create native encoder: #{inspect(reason)}" + end + end +end diff --git a/mix.exs b/mix.exs index 29f7638..88fe515 100644 --- a/mix.exs +++ b/mix.exs @@ -41,12 +41,8 @@ defmodule Membrane.VPx.Plugin.Mixfile do {:membrane_core, "~> 1.0"}, {:unifex, "~> 1.2"}, {:membrane_raw_video_format, "~> 0.4.0"}, - # {:membrane_vp8_format, "~> 0.4.0"}, - {:membrane_vp8_format, - github: "membraneframework/membrane_vp8_format", branch: "add-fields", override: true}, - # {:membrane_vp9_format, "~> 0.4.0"}, - {:membrane_vp9_format, - github: "membraneframework/membrane_vp9_format", branch: "add-fields", override: true}, + {:membrane_vp8_format, "~> 0.5.0"}, + {:membrane_vp9_format, "~> 0.5.0"}, {:membrane_precompiled_dependency_provider, "~> 0.1.0"}, # {:membrane_ivf_plugin, "~> 0.7.0", only: :test}, {:membrane_ivf_plugin, @@ -54,6 +50,7 @@ defmodule Membrane.VPx.Plugin.Mixfile do branch: "fix-plugin", override: true, only: :test}, + {:membrane_raw_video_parser_plugin, "~> 0.12.1", only: :test}, {:membrane_file_plugin, "~> 0.17.0", only: :test}, {:ex_doc, ">= 0.0.0", only: :dev, runtime: false}, {:dialyxir, ">= 0.0.0", only: :dev, runtime: false}, diff --git a/mix.lock b/mix.lock index 087e1ea..33e7098 100644 --- a/mix.lock +++ b/mix.lock @@ -5,12 +5,12 @@ "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.7", "b651241514e5f6956028147fe6637f7ac13802537e895a724f90bf3e36ddd1dd", [:mix], [], "hexpm", "da7785a4b0d2a021cd1292a60875a784b6caef71e76bf4917bdee1f390455cf5"}, "coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"}, - "credo": {:hex, :credo, "1.7.6", "b8f14011a5443f2839b04def0b252300842ce7388f3af177157c86da18dfbeea", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "146f347fb9f8cbc5f7e39e3f22f70acbef51d441baa6d10169dd604bfbc55296"}, + "credo": {:hex, :credo, "1.7.7", "771445037228f763f9b2afd612b6aa2fd8e28432a95dbbc60d8e03ce71ba4446", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8bc87496c9aaacdc3f90f01b7b0582467b69b4bd2441fe8aae3109d843cc2f2e"}, "dialyxir": {:hex, :dialyxir, "1.4.3", "edd0124f358f0b9e95bfe53a9fcf806d615d8f838e2202a9f430d59566b6b53b", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "bf2cfb75cd5c5006bec30141b131663299c661a864ec7fbbc72dfa557487a986"}, "earmark_parser": {:hex, :earmark_parser, "1.4.39", "424642f8335b05bb9eb611aa1564c148a8ee35c9c8a8bba6e129d51a3e3c6769", [:mix], [], "hexpm", "06553a88d1f1846da9ef066b87b57c6f605552cfbe40d20bd8d59cc6bde41944"}, "elixir_uuid": {:hex, :elixir_uuid, "1.2.1", "dce506597acb7e6b0daeaff52ff6a9043f5919a4c3315abb4143f0b00378c097", [:mix], [], "hexpm", "f7eba2ea6c3555cea09706492716b0d87397b88946e6380898c2889d68585752"}, - "erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"}, - "ex_doc": {:hex, :ex_doc, "0.34.0", "ab95e0775db3df71d30cf8d78728dd9261c355c81382bcd4cefdc74610bef13e", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "60734fb4c1353f270c3286df4a0d51e65a2c1d9fba66af3940847cc65a8066d7"}, + "erlex": {:hex, :erlex, "0.2.7", "810e8725f96ab74d17aac676e748627a07bc87eb950d2b83acd29dc047a30595", [:mix], [], "hexpm", "3ed95f79d1a844c3f6bf0cea61e0d5612a42ce56da9c03f01df538685365efb0"}, + "ex_doc": {:hex, :ex_doc, "0.34.1", "9751a0419bc15bc7580c73fde506b17b07f6402a1e5243be9e0f05a68c723368", [:mix], [{:earmark_parser, "~> 1.4.39", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "d441f1a86a235f59088978eff870de2e815e290e44a8bd976fe5d64470a4c9d2"}, "file_system": {:hex, :file_system, "1.0.0", "b689cc7dcee665f774de94b5a832e578bd7963c8e637ef940cd44327db7de2cd", [:mix], [], "hexpm", "6752092d66aec5a10e662aefeed8ddb9531d79db0bc145bb8c40325ca1d8536d"}, "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, @@ -20,12 +20,13 @@ "makeup_elixir": {:hex, :makeup_elixir, "0.16.2", "627e84b8e8bf22e60a2579dad15067c755531fea049ae26ef1020cad58fe9578", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "41193978704763f6bbe6cc2758b84909e62984c7752b3784bd3c218bb341706b"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.0", "6f0eff9c9c489f26b69b61440bf1b238d95badae49adac77973cbacae87e3c2e", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "ea7a9307de9d1548d2a72d299058d1fd2339e3d398560a0e46c27dab4891e4d2"}, "membrane_core": {:hex, :membrane_core, "1.1.0", "c3bbaa5af7c26a7c3748e573efe343c2104801e3463b9e491a607e82860334a4", [:mix], [{:bunch, "~> 1.6", [hex: :bunch, repo: "hexpm", optional: false]}, {:qex, "~> 0.3", [hex: :qex, repo: "hexpm", optional: false]}, {:ratio, "~> 3.0 or ~> 4.0", [hex: :ratio, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "b3209d7f7e86d736cb7caffbba16b075c571cebb9439ab939ed6119c50fb59a5"}, - "membrane_file_plugin": {:hex, :membrane_file_plugin, "0.17.0", "e855a848e84eaed537b41fd4436712038fc5518059eadc8609c83cd2d819653a", [:mix], [{:logger_backends, "~> 1.0", [hex: :logger_backends, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "9c3653ca9f13bb409b36257d6094798d4625c739ab7a4035c12308622eb16e0b"}, - "membrane_ivf_plugin": {:git, "https://github.com/membraneframework/membrane_ivf_plugin.git", "6fc0824073c1637b990b5e2eceb915a0f78e003c", [branch: "fix-plugin"]}, + "membrane_file_plugin": {:hex, :membrane_file_plugin, "0.17.1", "055a904823506e806e1e1a43643de2dfbe9baf3c1fe2f6f055d2e9b3710767dd", [:mix], [{:logger_backends, "~> 1.0", [hex: :logger_backends, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}], "hexpm", "0b209e17a7bafb8e281fe5d15b3760d9c6f8b3af628ed4589267ba01b7774d8f"}, + "membrane_ivf_plugin": {:git, "https://github.com/membraneframework/membrane_ivf_plugin.git", "e112040f22fe87dbe6142ee85c551abf202b426f", [branch: "fix-plugin"]}, "membrane_precompiled_dependency_provider": {:hex, :membrane_precompiled_dependency_provider, "0.1.2", "8af73b7dc15ba55c9f5fbfc0453d4a8edfb007ade54b56c37d626be0d1189aba", [:mix], [{:bundlex, "~> 1.4", [hex: :bundlex, repo: "hexpm", optional: false]}], "hexpm", "7fe3e07361510445a29bee95336adde667c4162b76b7f4c8af3aeb3415292023"}, - "membrane_raw_video_format": {:hex, :membrane_raw_video_format, "0.4.0", "99fdf3a5cd5f64118e550b80d79ffed0d4be7becb3878a657f8726550c8c756f", [:mix], [], "hexpm", "1768c8137e4cfc6470117f71318a1dd9726c3b305a78bd56d90f796be716bd99"}, - "membrane_vp8_format": {:git, "https://github.com/membraneframework/membrane_vp8_format.git", "b9d9e4d56d7f25c6ac341b2d4581445d22c1d190", [branch: "add-fields"]}, - "membrane_vp9_format": {:git, "https://github.com/membraneframework/membrane_vp9_format.git", "93b2d68f4a3c9b06a1a3d7246bfd9374b58935ea", [branch: "add-fields"]}, + "membrane_raw_video_format": {:hex, :membrane_raw_video_format, "0.4.1", "d7344499c2d80f236a7ef962b5490c651341a501052ee43dec56cf0319fa3936", [:mix], [], "hexpm", "9920b7d445b5357608a364fec5685acdfce85334c647f745045237a0d296c442"}, + "membrane_raw_video_parser_plugin": {:hex, :membrane_raw_video_parser_plugin, "0.12.2", "7a1f11e122dfc1481654fd5a9ac43db80f7851ad569662cfca2e8a818403101c", [:mix], [{:bunch, "~> 1.3", [hex: :bunch, repo: "hexpm", optional: false]}, {:membrane_core, "~> 1.0", [hex: :membrane_core, repo: "hexpm", optional: false]}, {:membrane_file_plugin, "~> 0.17.0", [hex: :membrane_file_plugin, repo: "hexpm", optional: false]}, {:membrane_raw_video_format, "~> 0.4.0", [hex: :membrane_raw_video_format, repo: "hexpm", optional: false]}], "hexpm", "c9254cc52c96ba0b575a65e4ab41f9218cef91ee5953cf6c1180835a21873907"}, + "membrane_vp8_format": {:hex, :membrane_vp8_format, "0.5.0", "a589c20bb9d97ddc9b717684d00cefc84e2500ce63a0c33c4b9618d9b2f9b2ea", [:mix], [], "hexpm", "d29e0dae4bebc6838e82e031c181fe626d168c687e4bc617c1d0772bdeed19d5"}, + "membrane_vp9_format": {:hex, :membrane_vp9_format, "0.5.0", "c6a4f2cbfc39dba5d80ad8287162c52b5cf6488676bd64435c1ac957bd16e66f", [:mix], [], "hexpm", "68752d8cbe7270ec222fc84a7d1553499f0d8ff86ef9d9e89f8955d49e20278e"}, "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, "mint": {:hex, :mint, "1.6.1", "065e8a5bc9bbd46a41099dfea3e0656436c5cbcb6e741c80bd2bad5cd872446f", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "4fc518dcc191d02f433393a72a7ba3f6f94b101d094cb6bf532ea54c89423780"}, "nimble_options": {:hex, :nimble_options, "1.1.1", "e3a492d54d85fc3fd7c5baf411d9d2852922f66e69476317787a7b2bb000a61b", [:mix], [], "hexpm", "821b2470ca9442c4b6984882fe9bb0389371b8ddec4d45a9504f00a66f650b44"}, diff --git a/test/fixtures/ref_vp8.ivf b/test/fixtures/ref_vp8.ivf new file mode 100644 index 0000000000000000000000000000000000000000..4b2e416300c1de393ee08de21e3c1f77381e82d6 GIT binary patch literal 32634 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�GEWS(VOw+>oM{b&H|_*jO!Es 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 literal 0 HcmV?d00001 diff --git a/test/fixtures/ref_vp9.ivf b/test/fixtures/ref_vp9.ivf new file mode 100644 index 0000000000000000000000000000000000000000..6df027821e7bc699ccb73ce2abefdf44bca92300 GIT binary patch literal 22218 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 I@!o*{13O;`c>n+a literal 0 HcmV?d00001 diff --git a/test/membrane_vpx_plugin/vpx_decoder_test.exs b/test/membrane_vpx_plugin/vpx_decoder_test.exs index 77d21c6..6a96526 100644 --- a/test/membrane_vpx_plugin/vpx_decoder_test.exs +++ b/test/membrane_vpx_plugin/vpx_decoder_test.exs @@ -14,7 +14,7 @@ defmodule Membrane.VPx.DecoderTest do "input_vp8.ivf", "output_vp8.raw", "ref_vp8.raw", - %Membrane.VP8.Decoder{framerate: {30, 1}} + %Membrane.VP8.Decoder{} ) end @@ -24,7 +24,7 @@ defmodule Membrane.VPx.DecoderTest do "input_vp9.ivf", "output_vp9.raw", "ref_vp9.raw", - %Membrane.VP9.Decoder{framerate: {30, 1}} + %Membrane.VP9.Decoder{} ) end end @@ -47,5 +47,7 @@ defmodule Membrane.VPx.DecoderTest do assert_end_of_stream(pid, :sink, :input, 2000) assert File.read!(ref_path) == File.read!(output_path) + + Membrane.Testing.Pipeline.terminate(pid) end end diff --git a/test/membrane_vpx_plugin/vpx_encoder_test.exs b/test/membrane_vpx_plugin/vpx_encoder_test.exs new file mode 100644 index 0000000..1843eb1 --- /dev/null +++ b/test/membrane_vpx_plugin/vpx_encoder_test.exs @@ -0,0 +1,61 @@ +defmodule Membrane.VPx.EncoderTest do + use ExUnit.Case, async: true + + import Membrane.Testing.Assertions + import Membrane.ChildrenSpec + + @fixtures_dir "test/fixtures" + + describe "Encoder encodes correctly for" do + @describetag :tmp_dir + test "VP8 codec", %{tmp_dir: tmp_dir} do + perform_encoder_test( + tmp_dir, + "ref_vp8.raw", + "output_vp8.ivf", + "ref_vp8.ivf", + %Membrane.VP8.Encoder{encoding_deadline: 0} + ) + end + + test "VP9 codec", %{tmp_dir: tmp_dir} do + perform_encoder_test( + tmp_dir, + "ref_vp9.raw", + "output_vp9.ivf", + "ref_vp9.ivf", + %Membrane.VP9.Encoder{encoding_deadline: 0} + ) + end + end + + defp perform_encoder_test(tmp_dir, input_file, output_file, ref_file, encoder_struct) do + output_path = Path.join(tmp_dir, output_file) + ref_path = Path.join(@fixtures_dir, ref_file) + + pid = + Membrane.Testing.Pipeline.start_link_supervised!( + spec: + child(:source, %Membrane.File.Source{ + location: Path.join(@fixtures_dir, input_file) + }) + |> child(:parser, %Membrane.RawVideo.Parser{ + pixel_format: :I420, + width: 1080, + height: 720, + framerate: {30, 1} + }) + |> child(:encoder, encoder_struct) + |> child(:serializer, %Membrane.IVF.Serializer{ + timebase: {1, 30} + }) + |> child(:sink, %Membrane.File.Sink{location: output_path}) + ) + + assert_end_of_stream(pid, :sink, :input, 10_000) + + assert File.read!(ref_path) == File.read!(output_path) + + Membrane.Testing.Pipeline.terminate(pid) + end +end