23.06.15
DEVOTEE를 활성화 시키면
지금 작성한 커뮤니티 글에 대해 1개의 댓글을 달아줍니다.
버튼을 누르면 글 수정 시 ChatGPT가 작성한 댓글이 수정됩니다.
| 컨텐츠 유형 | 제목 | 저장일 | 삭제 |
|---|
본인인증 로그인에 실패하였습니다.
회원이 아니시거나 본인인증 등록이
완료되지 않은 사용자입니다.
이전 글에서는 Chat GPT에 FFmpeg Custom Video Filter 만드는 방법을 물어 봤습니다.
결과물은 그럴싸하게 나왔으나, 정상적으로 동작하지는 않았어요. 역시나, 아직은 개발자가 필요하네요 ^^
그럼 이번엔 구글에 검색해서 FFmpeg Custom Video Filter 만드는 법을 찾아봤습니다.
위의 FFmpeg Document를 보니 Custom Video Filter 만드는 법이 잘 설명이 되어 있습니다.
되도록 위의 문서에 없는 내용을 설명해보도록 하겠습니다.
제가 요청 받은 FFmpeg의 Custom Video Filter 요구사항은 아래와 같습니다.
Decoding 된 영상을 ML기반 Python 엔진에서 필터링 후 다시 FFmpeg에 전달 할 수 있을 것
IPC로 프로세스 분리하여 동작 할 것
아래 그림과 같이 Custom Video Filter를 FFmpeg에 추가하고, IPC를 통해 외부 Python 엔진과 데이터를 주고 받을 수 있는 구조로 설계하였습니다.
이렇게 함으로써, FFmpeg의 모드 설비를 이용할 수 있고, ML기반의 Python엔진들을 간단하게 붙여서 동작 시킬 수 있으며, GPL 라이선스에 따른 소스 공개의무도 어느정도 우회 할 수 있었습니다 ^^;;.
IPC 설비로는 named pipe(FIFO)를 사용하였습니다. Windows 나 linux에서 모두 지원하고, 간단하게 구현가능한 장점이 있습니다.
우선 FFmpeg의 Filter구조를 보면 아래 5개의 함수만 구현 하면 됩니다.
init() | 초기화에 사용됨. 필요한 메모리 및 리소스 할당 |
uninit() | init()에서 할당된 리소스 해제 |
query_formats() | 지원하는 비디오 Pixel 포멧 정보 전달. Video Filter간 Pixel Format Negotiation에 사용됨. |
config_props() | config properties. 비디오 필터간 연결시 pad 로 연결이 되는데 이 때 호출됨. 입력 pad에서 Pixel Format Negotiation 된 포멧을 확인하거나, 출력 pad 의 영상 크기 등을 지정 할 수 있음. |
filter_frame() | 실제 필터링이 이루어지는 함수. 이전 필터로부터 데이터가 전달 될때 마다 호출된다. |
위의 그림과 같이 Filter는 입력 Pad와 출력 Pad를 통해 데이터를 받아 처리 후 다음 필터로 전달하게 되어 있다.
만약 Filter 1의 Output Pad의 Pixel 포멧이 AV_PIX_FMT_RGB32 와 같이 RGB로만 출력하는 경우,
Filter 2의 query_format()을 통한 지원하는 Pixel포멧을 확인해보니 AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV444P와 같이
지원하는 포멧이 다른 경우 해당 필터는 연결 할 수 없습니다.
또한, 지원하는 포멧이 여러개 인경우 Negotiation을 통해서 Pixel Format이 결정되게 됩니다.
Filter에 옵션을 전달 할 수 있도록 아래와 같이 추가 합니다.
간단히 설명하면 Bypass 모드나 IPC로 Python 엔진을 통해 필터링 할 것 인지 설정 할 수 있는 옵션과, 입출력 Named pipe의 경로를 지정하는 옵션을 추가 하였습니다.
#define OFFSET(x) offsetof(SupernovaContext, x)
#define FLAGS AV_OPT_FLAG_FILTERING_PARAM|AV_OPT_FLAG_VIDEO_PARAM
static const AVOption supernova_options[] = {
{ "mode", "set mode", OFFSET(mode), AV_OPT_TYPE_INT, {.i64=MODE_BYPASS}, 0, NB_MODES-1, FLAGS, "mode" },
{ "bypass", "bypass mode", 0, AV_OPT_TYPE_CONST, {.i64=MODE_BYPASS}, INT_MIN, INT_MAX, FLAGS, "mode" },
{ "ipc", "ipc mode", 0, AV_OPT_TYPE_CONST, {.i64=MODE_IPC}, INT_MIN, INT_MAX, FLAGS, "mode" },
{"in_pipe", "set supernova input pipe", OFFSET(in_pipe_path), AV_OPT_TYPE_STRING, {.str="supernova_in"}, 0, 0, FLAGS },
{"out_pipe", "set supernova output pipe", OFFSET(out_pipe_path), AV_OPT_TYPE_STRING, {.str="supernova_out"}, 0, 0, FLAGS },
{ NULL }
};위의 옵션들은 아래와 같이 FFmpeg CLI로 사용할 수 있습니다.
$ ffmpeg -i test.mp4 -vf supernova=mode=ipc:in_pipe=supernova_in:out_pipe=supernova_out output.mp4init 함수에서 ipc mode인 경우 named pipe를 생성하고, fork 후 python 엔진을 실행 합니다.
종료 시 signal 전달을 위해 parent와 child의 pid를 각각 저장 후, 종료시 시그널로 알림을 줄 수 있도록 하였습니다.
실제로는 성능 향상을 위해 Named pipe오픈 후, pipe의 버퍼 크기를 조정하여 영상과 같은 큰 사이즈의 버퍼를 전달할때 성능 향상이 될 수 있도록 추가 하였습니다.
static int init_pipe(AVFilterContext *ctx, SupernovaContext *supernova)
{
int ret;
ret = mkfifo(supernova->in_pipe_path, 0666);
if (ret < 0) {
av_log(ctx, AV_LOG_WARNING, "failed to make %s pipe %d. it would be already created.", supernova->in_pipe_path, ret);
}
ret = mkfifo(supernova->out_pipe_path, 0666);
if (ret < 0) {
av_log(ctx, AV_LOG_WARNING, "failed to make %s pipe %d. it would be already created.", supernova->out_pipe_path, ret);
}
pid_t ffmpeg_pid = getpid();
pid_t pid = fork();
if (pid == 0) {
av_log(ctx, AV_LOG_DEBUG, "start supernova engine");
char pid_str[32];
sprintf(pid_str, "%d", ffmpeg_pid);
ret = execl("/usr/bin/python", "python", supernova->supernova_engine,
"--in_fifo", supernova->in_pipe_path,
"--out_fifo", supernova->out_pipe_path,
"--parent_pid", pid_str,
NULL);
if (ret != 0) {
av_log(ctx, AV_LOG_ERROR, "failed to start supernova engine(%s) : %d\n",
supernova->supernova_engine, ret);
return ret;
}
} else {
supernova->engine_pid = pid;
av_log(ctx, AV_LOG_DEBUG, "waiting for supernova engine");
if ((supernova->in_pipe = open(supernova->in_pipe_path, O_WRONLY | O_NOCTTY)) < 0) {
av_log(ctx, AV_LOG_ERROR, "failed to open supernova input pipe : %s", supernova->in_pipe_path);
}
if ((supernova->out_pipe = open(supernova->out_pipe_path, O_RDONLY | O_NOCTTY)) < 0) {
av_log(ctx, AV_LOG_ERROR, "failed to open supernova output pipe : %s", supernova->out_pipe_path);
}
}
return 0;
}
static av_cold int init(AVFilterContext *ctx)
{
SupernovaContext *supernova = ctx->priv;
int ret = 0;
if (supernova->mode == MODE_IPC) {
ret = init_pipe(ctx, supernova);
}
return ret;
}uninit의 경우 pipe를 close하고 엔진에 SIGQUIT시그널을 전달하여 정상 종료 되도록 신호를 보내고, 종료 될때 까지 대기 합니다. 또한 할당된 리소스들도 해제 합니다.
static void uninit_pipe(AVFilterContext *ctx, SupernovaContext *supernova)
{
close(supernova->in_pipe);
close(supernova->out_pipe);
int ret = 0;
ret = kill(supernova->engine_pid, 3); // 3 : SIGQUIT
if (ret ) {
kill(supernova->engine_pid, 9);
}
int status;
while (waitpid(supernova->engine_pid, &status, WNOHANG) > 0)
usleep(10*1000);
unlink(supernova->in_pipe_path);
unlink(supernova->out_pipe_path);
}
static av_cold void uninit(AVFilterContext *ctx)
{
SupernovaContext *supernova = ctx->priv;
if (supernova->mode == MODE_IPC) {
uninit_pipe(ctx, supernova);
}
if (supernova->temp_data) {
free(supernova->temp_data);
supernova->temp_data = NULL;
}
}본 필터가 지원하는 포멧을 물어볼때 아래와 같이 AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV444P 포멧을 지원한다고 알려줍니다.
static int query_formats(AVFilterContext *ctx)
{
const SupernovaContext *supernova = ctx->priv;
static const enum AVPixelFormat pix_fmts[] = {AV_PIX_FMT_YUV420P, AV_PIX_FMT_YUV422P, AV_PIX_FMT_YUV444P, AV_PIX_FMT_NONE};
return ff_set_common_formats_from_list(ctx, pix_fmts);
}해당 필터가 영상의 해상도를 2배 Scaling 하는 모듈인 경우, 아래와 같이 출력 링크의 해상도를 입력의 2배로 설정합니다.
static int config_props(AVFilterLink *outlink)
{
AVFilterLink *inlink = outlink->src->inputs[0];
enum AVPixelFormat outfmt = outlink->format;
outlink->w = inlink->w * 2;
outlink->h = inlink->h * 2;
outlink->sample_aspect_ratio = inlink->sample_aspect_ratio;
outlink->time_base = inlink->time_base;
return 0;
}실제 필터링이 이루어지는 함수입니다. bypass모드인 경우 입력 영상을 바로 출력 버퍼에 복사하여 출력하고,
IPC모드 인경우 Pipe에 write() 후 python엔진에서 필터링을 수행하고 다시 read()로 영상을 수신하여 다시 출력 버퍼에 복사하는 방법을 이용하였습니다.
우선 테스트로 temp_data에 메모리를 할당하고 이 메모리를 python엔진에 전달 및 수신하는 방법을 이용하였습니다.
read(), write()함수는 기본적으로는 block모드로 동작하므로, 종료시 block모드에서 나올 수 가 없어서 강제종료해야하는 경우가 있습니다.
이를 방지하게 위해 실제 구현에서는 select(), epoll()등을 이용한 multiplex IO나 non-block 모드로 read()/write()함수를 동작시켜야 합니다.
필터링이 끝난 버퍼를 다시 출력 버퍼에 복사 시에는 av_image_copy_plane()을 이용하였습니다.
영상의 복사 시에는 av_image_copy(), av_image_copy_to_buffer() 등 다양한 함수들이 있으므로 적절한 함수를 이용하면 됩니다.
필터링이 끝난 영상은 ff_filter_frame()함수로 다음 필터로 영상을 전달 합니다.
static int filter_frame(AVFilterLink *inlink, AVFrame *in)
{
AVFilterContext *ctx = inlink->dst;
SupernovaContext *supernova = ctx->priv;
AVFilterLink *outlink = ctx->outputs[0];
AVFrame *out;
int nb_planes = av_pix_fmt_count_planes(inlink->format);
int ret;
int chroma_x_shift, chroma_y_shift;
ret = av_pix_fmt_get_chroma_sub_sample(inlink->format, &chroma_x_shift, &chroma_y_shift);
if (ret)
return ret;
out = ff_get_video_buffer(outlink, outlink->w, outlink->h);
if (!out) {
av_frame_free(&in);
return AVERROR(ENOMEM);
}
av_frame_copy_props(out, in);
// filtering
if (supernova->mode == MODE_BYPASS) {
for (int p = 0; p < nb_planes; p++) {
int x_shift = (p == 0) ? 0 : chroma_x_shift;
int y_shift = (p == 0) ? 0 : chroma_y_shift;
av_image_copy_plane(
out->data[p],
out->linesize[p],
in->data[p],
in->linesize[p],
in->width >> x_shift,
in->height >> y_shift
);
}
} else if (supernova->mode == MODE_IPC) {
int offset = 0;
int length = 0;
int size = (inlink->w * inlink->h) + (inlink->w >> chroma_x_shift) * (inlink->h >> chroma_y_shift) * (nb_planes - 1);
if (supernova->temp_data == NULL) {
supernova->temp_data = (uint8_t*)malloc(size);
}
offset = 0;
for (int p = 0; p < nb_planes; p++) {
int x_shift = (p == 0) ? 0 : chroma_x_shift;
int y_shift = (p == 0) ? 0 : chroma_y_shift;
av_image_copy_plane(
supernova->temp_data + offset,
in->width >> x_shift,
in->data[p],
in->linesize[p],
in->width >> x_shift,
in->height >> y_shift
);
offset += ((in->width >> x_shift) * (in->height >> y_shift));
}
offset = 0;
while (offset < size) {
length = write(
supernova->in_pipe,
supernova->temp_data + offset,
size - offset
);
if (length < 0) {
av_log(ctx, AV_LOG_ERROR, "could not write buffer : %d", length);
return AVERROR_EXTERNAL;
} else if (length == 0) {
av_log(ctx, AV_LOG_INFO, "got EoF");
return AVERROR_EOF;
}
offset += length;
}
av_log(ctx, AV_LOG_TRACE, "frame(%ld) write done", inlink->frame_count_in);
memset(supernova->temp_data, 0, size * sizeof(uint8_t));
offset = 0;
while (offset < size) {
length = read(
supernova->out_pipe,
supernova->temp_data + offset,
size - offset
);
if (length <= 0) {
av_log(ctx, AV_LOG_ERROR, "could not read buffer : %d", length);
return AVERROR_EXTERNAL;
} else if (length == 0) {
av_log(ctx, AV_LOG_INFO, "got EoF");
return AVERROR_EOF;
}
offset += length;
}
offset = 0;
for (int p = 0; p < nb_planes; p++) {
int x_shift = (p == 0) ? 0 : chroma_x_shift;
int y_shift = (p == 0) ? 0 : chroma_y_shift;
av_image_copy_plane(
out->data[p],
out->linesize[p],
supernova->temp_data + offset,
in->width >> x_shift,
in->width >> x_shift,
in->height >> y_shift
);
offset += ((in->width >> x_shift) * (in->height >> y_shift));
}
av_log(ctx, AV_LOG_TRACE, "frame(%ld) read done", outlink->frame_count_in);
}
if (in != out)
av_frame_free(&in);
return ff_filter_frame(outlink, out);
}위에 구현된 함수들을 입력 출력 AVFilterPad 구조체 및 AVFilter 구조체에 등록합니다. 이후 위의 FFmpeg Docs - Writing Filters를 참조하여 makefile 등을 수정후 빌드 하면 됩니다.
static const AVFilterPad supernova_inputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_VIDEO,
.filter_frame = filter_frame,
},
};
static const AVFilterPad supernova_outputs[] = {
{
.name = "default",
.type = AVMEDIA_TYPE_VIDEO,
.config_props = config_props,
},
};
const AVFilter ff_vf_supernova = {
.name = "supernova",
.description = NULL_IF_CONFIG_SMALL("SKT Supernova"),
.priv_size = sizeof(SupernovaContext),
.init = init,
.uninit = uninit,
FILTER_INPUTS(supernova_inputs),
FILTER_OUTPUTS(supernova_outputs),
FILTER_QUERY_FUNC(query_formats),
.priv_class = &supernova_class,
.flags = AVFILTER_FLAG_SUPPORT_TIMELINE_GENERIC,
};다음번에는 python 부분의 코드를 확인해보도록 하겠습니다.
DEVOTEE를 활성화 시키면
지금 작성한 댓글에 AI가 댓글을 달아줍니다.