diff --git a/c_src/xav/encoder.c b/c_src/xav/encoder.c index 757c429..8e3a8cf 100644 --- a/c_src/xav/encoder.c +++ b/c_src/xav/encoder.c @@ -16,10 +16,7 @@ struct Encoder *encoder_alloc() { } int encoder_init(struct Encoder *encoder, struct EncoderConfig *config) { - encoder->codec = avcodec_find_encoder(config->codec); - if (!encoder->codec) { - return -1; - } + encoder->codec = config->codec; encoder->c = avcodec_alloc_context3(encoder->codec); if (!encoder->c) { @@ -44,7 +41,7 @@ int encoder_init(struct Encoder *encoder, struct EncoderConfig *config) { } AVDictionary *opts = NULL; - if (config->codec == AV_CODEC_ID_HEVC) { + if (strcmp(encoder->codec->name, "libx265") == 0) { char x265_params[256] = "log-level=warning"; if (config->gop_size > 0) { sprintf(x265_params + strlen(x265_params), ":keyint=%d", config->gop_size); diff --git a/c_src/xav/encoder.h b/c_src/xav/encoder.h index 5f540be..9bb58d2 100644 --- a/c_src/xav/encoder.h +++ b/c_src/xav/encoder.h @@ -11,7 +11,7 @@ struct Encoder { struct EncoderConfig { enum AVMediaType media_type; - enum AVCodecID codec; + const AVCodec *codec; int width; int height; enum AVPixelFormat format; diff --git a/c_src/xav/utils.c b/c_src/xav/utils.c index b106092..b849e2a 100644 --- a/c_src/xav/utils.c +++ b/c_src/xav/utils.c @@ -35,6 +35,20 @@ int xav_nif_get_atom(ErlNifEnv *env, ERL_NIF_TERM term, char **value) { return 1; } +int xav_nif_get_string(ErlNifEnv *env, ERL_NIF_TERM term, char **value) { + ErlNifBinary bin; + if (!enif_inspect_binary(env, term, &bin)) { + return 0; + } + + char *str_value = (char *)XAV_ALLOC((bin.size + 1) * sizeof(char *)); + memcpy(str_value, bin.data, bin.size); + str_value[bin.size] = '\0'; + + *value = str_value; + return 1; +} + ERL_NIF_TERM xav_nif_audio_frame_to_term(ErlNifEnv *env, uint8_t **out_data, int out_samples, int out_size, enum AVSampleFormat out_format, int pts) { ERL_NIF_TERM data_term; diff --git a/c_src/xav/utils.h b/c_src/xav/utils.h index 9837a3c..6ecf4cd 100644 --- a/c_src/xav/utils.h +++ b/c_src/xav/utils.h @@ -21,6 +21,7 @@ ERL_NIF_TERM xav_nif_ok(ErlNifEnv *env, ERL_NIF_TERM data_term); ERL_NIF_TERM xav_nif_error(ErlNifEnv *env, char *reason); ERL_NIF_TERM xav_nif_raise(ErlNifEnv *env, char *msg); int xav_nif_get_atom(ErlNifEnv *env, ERL_NIF_TERM term, char **value); +int xav_nif_get_string(ErlNifEnv *env, ERL_NIF_TERM term, char **value); ERL_NIF_TERM xav_nif_video_frame_to_term(ErlNifEnv *env, AVFrame *frame); ERL_NIF_TERM xav_nif_audio_frame_to_term(ErlNifEnv *env, uint8_t **out_data, int out_samples, int out_size, enum AVSampleFormat out_format, int pts); diff --git a/c_src/xav/xav_decoder.c b/c_src/xav/xav_decoder.c index 95200b3..e884a22 100644 --- a/c_src/xav/xav_decoder.c +++ b/c_src/xav/xav_decoder.c @@ -303,10 +303,13 @@ ERL_NIF_TERM list_decoders(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) while ((codec = av_codec_iterate(&iter))) { if (av_codec_is_decoder(codec)) { ERL_NIF_TERM name = enif_make_atom(env, codec->name); - ERL_NIF_TERM long_name = enif_make_string(env, codec->long_name, ERL_NIF_LATIN1); + ERL_NIF_TERM codec_name = enif_make_atom(env, avcodec_get_name(codec->id)); + ERL_NIF_TERM long_name = codec->long_name + ? enif_make_string(env, codec->long_name, ERL_NIF_LATIN1) + : enif_make_string(env, "", ERL_NIF_LATIN1); ERL_NIF_TERM media_type = enif_make_atom(env, av_get_media_type_string(codec->type)); - ERL_NIF_TERM desc = enif_make_tuple3(env, name, long_name, media_type); + ERL_NIF_TERM desc = enif_make_tuple4(env, codec_name, name, long_name, media_type); result = enif_make_list_cell(env, desc, result); } } diff --git a/c_src/xav/xav_encoder.c b/c_src/xav/xav_encoder.c index 07b01a8..f987195 100644 --- a/c_src/xav/xav_encoder.c +++ b/c_src/xav/xav_encoder.c @@ -4,6 +4,7 @@ ErlNifResourceType *xav_encoder_resource_type; static ERL_NIF_TERM packets_to_term(ErlNifEnv *, struct Encoder *); static int get_profile(enum AVCodecID, const char *); +static ERL_NIF_TERM get_codec_profiles(ErlNifEnv *, const AVCodec *); ERL_NIF_TERM new (ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { if (argc != 2) { @@ -15,14 +16,15 @@ ERL_NIF_TERM new (ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { encoder_config.max_b_frames = -1; encoder_config.profile = FF_PROFILE_UNKNOWN; - char *codec = NULL, *format = NULL, *profile = NULL; + char *codec_name = NULL, *format = NULL, *profile = NULL; + int codec_id = 0; ErlNifMapIterator iter; ERL_NIF_TERM key, value; char *config_name = NULL; int err; - if (!xav_nif_get_atom(env, argv[0], &codec)) { + if (!xav_nif_get_atom(env, argv[0], &codec_name)) { return xav_nif_raise(env, "failed_to_get_atom"); } @@ -30,17 +32,6 @@ ERL_NIF_TERM new (ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { return xav_nif_raise(env, "failed_to_get_map"); } - if (strcmp(codec, "h264") == 0) { - encoder_config.media_type = AVMEDIA_TYPE_VIDEO; - encoder_config.codec = AV_CODEC_ID_H264; - } else if (strcmp(codec, "h265") == 0 || strcmp(codec, "hevc") == 0) { - encoder_config.media_type = AVMEDIA_TYPE_VIDEO; - encoder_config.codec = AV_CODEC_ID_HEVC; - } else { - ret = xav_nif_raise(env, "failed_to_resolve_codec"); - goto clean; - } - enif_map_iterator_create(env, argv[1], &iter, ERL_NIF_MAP_ITERATOR_FIRST); while (enif_map_iterator_get_pair(env, &iter, &key, &value)) { @@ -64,7 +55,9 @@ ERL_NIF_TERM new (ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { } else if (strcmp(config_name, "max_b_frames") == 0) { err = enif_get_int(env, value, &encoder_config.max_b_frames); } else if (strcmp(config_name, "profile") == 0) { - err = xav_nif_get_atom(env, value, &profile); + err = xav_nif_get_string(env, value, &profile); + } else if (strcmp(config_name, "codec_id") == 0) { + err = enif_get_int(env, value, &codec_id); } else { ret = xav_nif_raise(env, "unknown_config_key"); goto clean; @@ -79,6 +72,17 @@ ERL_NIF_TERM new (ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { enif_map_iterator_next(env, &iter); } + if (strcmp(codec_name, "nil") == 0) { + encoder_config.codec = avcodec_find_encoder((enum AVCodecID)codec_id); + } else { + encoder_config.codec = avcodec_find_encoder_by_name(codec_name); + } + + if (!encoder_config.codec) { + ret = xav_nif_raise(env, "unknown_codec"); + goto clean; + } + encoder_config.format = av_get_pix_fmt(format); if (encoder_config.format == AV_PIX_FMT_NONE) { ret = xav_nif_raise(env, "unknown_format"); @@ -86,7 +90,7 @@ ERL_NIF_TERM new (ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { } if (profile) { - encoder_config.profile = get_profile(encoder_config.codec, profile); + encoder_config.profile = get_profile(encoder_config.codec->id, profile); if (encoder_config.profile == FF_PROFILE_UNKNOWN) { ret = xav_nif_raise(env, "invalid_profile"); goto clean; @@ -107,8 +111,8 @@ ERL_NIF_TERM new (ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { enif_release_resource(xav_encoder); clean: - if (!codec) - XAV_FREE(codec); + if (!codec_name) + XAV_FREE(codec_name); if (!format) XAV_FREE(format); if (!config_name) @@ -178,6 +182,32 @@ ERL_NIF_TERM flush(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { return packets_to_term(env, xav_encoder->encoder); } +ERL_NIF_TERM list_encoders(ErlNifEnv *env, int argc, const ERL_NIF_TERM argv[]) { + ERL_NIF_TERM result = enif_make_list(env, 0); + + const AVCodec *codec = NULL; + void *iter = NULL; + + while ((codec = av_codec_iterate(&iter))) { + if (av_codec_is_encoder(codec)) { + ERL_NIF_TERM name = enif_make_atom(env, codec->name); + ERL_NIF_TERM codec_name = enif_make_atom(env, avcodec_get_name(codec->id)); + ERL_NIF_TERM long_name = codec->long_name + ? enif_make_string(env, codec->long_name, ERL_NIF_LATIN1) + : enif_make_string(env, "", ERL_NIF_LATIN1); + ERL_NIF_TERM media_type = enif_make_atom(env, av_get_media_type_string(codec->type)); + ERL_NIF_TERM codec_id = enif_make_int64(env, codec->id); + ERL_NIF_TERM profiles = get_codec_profiles(env, codec); + + ERL_NIF_TERM desc = + enif_make_tuple6(env, codec_name, name, long_name, media_type, codec_id, profiles); + result = enif_make_list_cell(env, desc, result); + } + } + + return result; +} + void free_xav_encoder(ErlNifEnv *env, void *obj) { XAV_LOG_DEBUG("Freeing XavEncoder object"); struct XavEncoder *xav_encoder = (struct XavEncoder *)obj; @@ -208,32 +238,48 @@ static ERL_NIF_TERM packets_to_term(ErlNifEnv *env, struct Encoder *encoder) { } static int get_profile(enum AVCodecID codec, const char *profile_name) { - if (codec == AV_CODEC_ID_H264) { - if (strcmp(profile_name, "constrained_baseline") == 0) { - return FF_PROFILE_H264_CONSTRAINED_BASELINE; - } else if (strcmp(profile_name, "baseline") == 0) { - return FF_PROFILE_H264_BASELINE; - } else if (strcmp(profile_name, "main") == 0) { - return FF_PROFILE_H264_MAIN; - } else if (strcmp(profile_name, "high") == 0) { - return FF_PROFILE_H264_HIGH; - } + const AVCodecDescriptor *desc = avcodec_descriptor_get(codec); + const AVProfile *profile = desc->profiles; + + if (profile == NULL) { + return FF_PROFILE_UNKNOWN; } - if (codec == AV_CODEC_ID_HEVC) { - if (strcmp(profile_name, "main") == 0) { - return FF_PROFILE_HEVC_MAIN; - } else if (strcmp(profile_name, "main_10") == 0) { - return FF_PROFILE_HEVC_MAIN_10; - } else if (strcmp(profile_name, "main_still_picture") == 0) { - return FF_PROFILE_HEVC_MAIN_STILL_PICTURE; + while (profile->profile != FF_PROFILE_UNKNOWN) { + if (strcmp(profile->name, profile_name) == 0) { + break; } + + profile++; + } + + return profile->profile; +} + +static ERL_NIF_TERM get_codec_profiles(ErlNifEnv *env, const AVCodec *codec) { + ERL_NIF_TERM result = enif_make_list(env, 0); + + const AVCodecDescriptor *desc = avcodec_descriptor_get(codec->id); + const AVProfile *profile = desc->profiles; + + if (profile == NULL) { + return result; + } + + while (profile->profile != FF_PROFILE_UNKNOWN) { + ERL_NIF_TERM profile_name = enif_make_string(env, profile->name, ERL_NIF_LATIN1); + result = enif_make_list_cell(env, profile_name, result); + + profile++; } - return FF_PROFILE_UNKNOWN; + return result; } -static ErlNifFunc xav_funcs[] = {{"new", 2, new}, {"encode", 3, encode}, {"flush", 1, flush}}; +static ErlNifFunc xav_funcs[] = {{"new", 2, new}, + {"encode", 3, encode}, + {"flush", 1, flush}, + {"list_encoders", 0, list_encoders}}; static int load(ErlNifEnv *env, void **priv, ERL_NIF_TERM load_info) { xav_encoder_resource_type = diff --git a/lib/xav.ex b/lib/xav.ex index 9b8c633..b8914cc 100644 --- a/lib/xav.ex +++ b/lib/xav.ex @@ -1,6 +1,21 @@ defmodule Xav do @moduledoc File.read!("README.md") + @type encoder :: %{ + codec: atom(), + name: atom(), + long_name: String.t(), + media_type: atom(), + profiles: [String.t()] + } + + @type decoder :: %{ + codec: atom(), + name: atom(), + long_name: String.t(), + media_type: atom() + } + @doc """ Get all available pixel formats. @@ -24,17 +39,35 @@ defmodule Xav do @doc """ List all decoders. - - The result is a list of 3-element tuples `{name, long_name, media_type}`: - * `name` - The short name of the decoder. - * `long_name` - The long name of the decoder. - * `media_type` - The media type of the decoder. """ - @spec list_decoders() :: [{name :: atom(), long_name :: String.t(), media_type :: atom()}] + @spec list_decoders() :: [decoder()] def list_decoders() do Xav.Decoder.NIF.list_decoders() - |> Enum.map(fn {name, long_name, media_type} -> - {name, List.to_string(long_name), media_type} + |> Enum.map(fn {codec, name, long_name, media_type} -> + %{ + codec: codec, + name: name, + long_name: List.to_string(long_name), + media_type: media_type + } + end) + |> Enum.reverse() + end + + @doc """ + List all encoders. + """ + @spec list_encoders() :: [encoder()] + def list_encoders() do + Xav.Encoder.NIF.list_encoders() + |> Enum.map(fn {family_name, name, long_name, media_type, _codec_id, profiles} -> + %{ + codec: family_name, + name: name, + long_name: List.to_string(long_name), + media_type: media_type, + profiles: profiles |> Enum.map(&List.to_string/1) |> Enum.reverse() + } end) |> Enum.reverse() end diff --git a/lib/xav/encoder.ex b/lib/xav/encoder.ex index dbb710a..7e46d8c 100644 --- a/lib/xav/encoder.ex +++ b/lib/xav/encoder.ex @@ -2,18 +2,14 @@ defmodule Xav.Encoder do @moduledoc """ Audio/Video encoder. - Currently, it only supports video encoding: - * `h264` - * `h265`/`hevc` + Currently, it only supports video encoding. """ @type t :: reference() - @type codec :: :h264 | :h265 | :hevc + @type codec :: atom() @type encoder_options :: Keyword.t() - @video_codecs [:h264, :h265, :hevc] - @video_encoder_schema [ width: [ type: :pos_integer, @@ -63,21 +59,14 @@ defmodule Xav.Encoder do """ ], profile: [ - type: - {:in, [:constrained_baseline, :baseline, :main, :high, :main_10, :main_still_picture]}, - type_doc: "`t:atom/0`", + type: :string, doc: """ The encoder's profile. A profile defines the capabilities and features an encoder can use to target specific applications (e.g. `live video`) - The following profiles are defined: - - | Codec | Profiles | - |-------|----------| - | h264 | constrained_baseline, baseline, main, high | - | h265/hevc | main, main_10, main_still_picture | + To get the list of available profiles for an encoder, see `Xav.list_encoders/0` """ ] ] @@ -85,10 +74,14 @@ defmodule Xav.Encoder do @doc """ Create a new encoder. + To get the list of available encoders, see `Xav.list_encoders/0`. + It accepts the following options:\n#{NimbleOptions.docs(@video_encoder_schema)} """ @spec new(codec(), Keyword.t()) :: t() - def new(codec, opts) when codec in @video_codecs do + def new(codec, opts) do + {codec, codec_id} = validate_codec!(codec) + opts = NimbleOptions.validate!(opts, @video_encoder_schema) {time_base_num, time_base_den} = opts[:time_base] @@ -98,7 +91,11 @@ defmodule Xav.Encoder do |> Map.delete(:time_base) |> Map.merge(%{time_base_num: time_base_num, time_base_den: time_base_den}) - Xav.Encoder.NIF.new(codec, nif_options) + if codec_id do + Xav.Encoder.NIF.new(nil, Map.put(nif_options, :codec_id, codec_id)) + else + Xav.Encoder.NIF.new(codec, nif_options) + end end @doc """ @@ -129,4 +126,20 @@ defmodule Xav.Encoder do %Xav.Packet{data: data, dts: dts, pts: pts, keyframe?: keyframe?} end) end + + defp validate_codec!(codec) do + Xav.Encoder.NIF.list_encoders() + |> Enum.find_value(fn {codec_family, encoder_name, _, media_type, codec_id, _profiles} -> + cond do + media_type != :video -> nil + encoder_name == codec -> {encoder_name, nil} + codec_family == codec -> {codec, codec_id} + true -> nil + end + end) + |> case do + nil -> raise ArgumentError, "Unknown codec: #{inspect(codec)}" + result -> result + end + end end diff --git a/lib/xav/encoder_nif.ex b/lib/xav/encoder_nif.ex index 12dc90a..fa90557 100644 --- a/lib/xav/encoder_nif.ex +++ b/lib/xav/encoder_nif.ex @@ -13,4 +13,6 @@ defmodule Xav.Encoder.NIF do def encode(_encoder, _data, _pts), do: :erlang.nif_error(:undef) def flush(_encoder), do: :erlang.nif_error(:undef) + + def list_encoders(), do: :erlang.nif_error(:undef) end diff --git a/test/encoder_test.exs b/test/encoder_test.exs index 7c9cdfc..2b14b67 100644 --- a/test/encoder_test.exs +++ b/test/encoder_test.exs @@ -17,7 +17,7 @@ defmodule Xav.EncoderTest do end test "raises on invalid encoder" do - assert_raise FunctionClauseError, fn -> Xav.Encoder.new(:h263, []) end + assert_raise ArgumentError, fn -> Xav.Encoder.new(:h264_none, []) end end test "raises on invalid options" do @@ -109,7 +109,7 @@ defmodule Xav.EncoderTest do |> Enum.to_list() assert length(packets) == 20 - Enum.all?(packets, &(&1.dts == &1.pts)) + assert Enum.all?(packets, &(&1.dts == &1.pts)), "dts should be equal to pts" end end end