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 0000000..4b2e416 Binary files /dev/null and b/test/fixtures/ref_vp8.ivf differ diff --git a/test/fixtures/ref_vp9.ivf b/test/fixtures/ref_vp9.ivf new file mode 100644 index 0000000..6df0278 Binary files /dev/null and b/test/fixtures/ref_vp9.ivf differ 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