🧑‍💻

RustでFFmpegフィルターを簡単に実装:音声・動画処理をシンプルにする新手法

に公開

はじめに

FFmpegは、動画や音声のエンコード、デコード、トランスコード、フィルター処理に広く使われる強力なツールです。しかし、RustプロジェクトでFFmpegのC APIを直接使うと、メモリ管理の複雑さやセキュリティリスクに直面することがあります。特にカスタムフィルターを実装する場合、従来の方法ではCコードを書き、FFmpegの内部構造を深く理解する必要があり、多くの開発者にとってハードルが高いです。Rustのメモリ安全性とシンプルさを利用して、ez-ffmpegライブラリを使えば、純粋なRustコードでFFmpegのカスタムフィルターを実装でき、開発の難易度を大幅に下げることができます。

この記事では、Rustとez-ffmpegを使ってFFmpegのカスタムフィルターを実装する方法を、動画と音声の処理に焦点を当てて詳しく解説します。初心者から上級者まで、役立つ内容をお届けします。


痛点とシーン分析

従来の方法の課題

  • 複雑さ:FFmpegのC APIはメモリ管理を手動で行う必要があり、ミスするとメモリリークやクラッシュの原因に。
  • 安全性:RustからCコードを呼び出すにはFFI(外部関数インターフェース)が必要で、unsafeブロックが増え、安全性が損なわれる。
  • 学習の難しさ:カスタムフィルターの実装には、FFmpegのフィルターグラフやフレーム処理の仕組みを理解しなければならず、C言語に不慣れな開発者には特に大変。

適用シーン

  • リアルタイム動画処理:ライブ配信で明るさ調整やグレースケールエフェクトを追加。
  • 機械学習のデータ拡張:動画フレームを変換して多様なトレーニングデータを作成。
  • ゲーム開発:動画コンテンツにダイナミックなエフェクトを追加し、視覚体験を向上。
  • 音声処理:音量調整やエフェクトを追加し、聴覚体験を最適化。
  • 監視システム:動画分析で動き検出やオブジェクト追跡を実現。

これらのシーンでは、効率的で安全、かつ使いやすいツールが求められます。ez-ffmpegはRustの特性を活かして、このニーズに応えます。


基本的な実装:明るさ調整フィルター(YUV420)

まずは、YUV420フォーマットの動画に明るさ調整フィルターを適用する基本的な例から始めましょう。YUV420は一般的な動画フォーマットなので、実用的なスタート地点です。

環境設定

Cargo.tomlに依存関係を追加:

[dependencies]
ez-ffmpeg = "*"

システムにFFmpeg 7.0以降の依存ライブラリ(実行ファイルではない)がインストールされていること、Rustのバージョンが1.80.0以降であることを確認してください。

実装コード

以下のコードは、Y成分を増やして明るさを調整します:

use ez_ffmpeg::core::filter::frame_filter::FrameFilter;
use ez_ffmpeg::filter::frame_filter_context::FrameFilterContext;
use ez_ffmpeg::{AVMediaType, Frame};

pub struct BrightnessFilter {
    pub(crate) increment: i32,
}

impl FrameFilter for BrightnessFilter {
    fn media_type(&self) -> AVMediaType {
        AVMediaType::AVMEDIA_TYPE_VIDEO
    }

    fn filter_frame(
        &mut self,
        mut frame: Frame,
        _ctx: &FrameFilterContext,
    ) -> Result<Option<Frame>, String> {
        if unsafe { frame.as_ptr().is_null() } {
            println!("終了フレームを受け取りました");
            return Ok(Some(frame));
        }
        // YUV420Pのみをサポート
        if unsafe { (*frame.as_ptr()).format } != 0 {
            return Err("サポートされていないピクセルフォーマット".to_string());
        }
        let y_data = plane_mut(&mut frame);
        for y in y_data.iter_mut() {
            let new_y = (*y as i32 + self.increment).clamp(0, 255) as u8;
            *y = new_y;
        }
        Ok(Some(frame))
    }
}

#[inline]
pub fn plane_mut(frame: &mut Frame) -> &mut [u8] {
    unsafe {
        std::slice::from_raw_parts_mut(
            (*frame.as_mut_ptr()).data[0],
            (*frame.as_mut_ptr()).linesize[0] as usize * (*frame.as_mut_ptr()).height as usize,
        )
    }
}

完全な実行コード

フィルターを動画処理のフローに適用します:

use crate::brightness_filter::BrightnessFilter;
use ez_ffmpeg::filter::frame_pipeline_builder::FramePipelineBuilder;
use ez_ffmpeg::{AVMediaType, FfmpegContext, Output};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let frame_pipeline_builder: FramePipelineBuilder = AVMediaType::AVMEDIA_TYPE_VIDEO.into();
    let brightness_filter = BrightnessFilter { increment: 20 };
    let frame_pipeline_builder = frame_pipeline_builder
        .filter("brightness", Box::new(brightness_filter))
        .build();

    FfmpegContext::builder()
        .input("input.mp4")
        .output(Output::from("output.mp4").add_frame_pipeline(frame_pipeline_builder))
        .build()?
        .start()?
        .wait()?;
    Ok(())
}

この例では、入力動画の明るさを20上げて、新しいファイルoutput.mp4を生成します。


より深い痛点と高度な実装

基本的な実装ではCPU処理を紹介しましたが、リアルタイム性やパフォーマンスが求められるシーンでは、CPU処理がボトルネックになることがあります。また、音声処理の需要も高まっています。以下では、GPUアクセラレーションを利用したグレースケールフィルターと、音声の音量調整フィルターの2つの高度な例を紹介します。

より深い痛点

  • パフォーマンスのボトルネック:CPUで大量の動画フレームを処理すると、特にリアルタイムアプリケーションで遅延が発生。
  • ハードウェアの互換性:GPUアクセラレーションでは、異なるハードウェアの互換性問題に対処する必要がある。
  • マルチメディアの要求:動画と音声の統合処理には、柔軟なツールが必要。

高度な例1:グレースケールフィルター(OpenGL)

高パフォーマンスが求められる場合、OpenGLを使ってGPUアクセラレーションによるグレースケールフィルターを実装できます。まず、opengl機能を有効にします:

[dependencies]
ez-ffmpeg = { version = "*", features = ["opengl"] }

opengl機能を有効にすると、ライブラリに含まれるOpenGLFrameFilterを直接使えます。自分でフィルターを実装する必要はありません。

フラグメントシェーダー

#version 330 core
in vec2 TexCoord;
out vec4 FragColor;
uniform sampler2D texture1;

void main() {
    vec4 color = texture(texture1, TexCoord);
    float gray = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
    FragColor = vec4(gray, gray, gray, color.a);
}

Rustの実装

use ez_ffmpeg::filter::frame_pipeline_builder::FramePipelineBuilder;
use ez_ffmpeg::opengl::opengl_frame_filter::OpenGLFrameFilter;
use ez_ffmpeg::{AVMediaType, FfmpegContext, Output};

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let fragment_shader = r#"
    #version 330 core
    in vec2 TexCoord;
    out vec4 FragColor;
    uniform sampler2D texture1;

    void main() {
        vec4 color = texture(texture1, TexCoord);
        float gray = 0.299 * color.r + 0.587 * color.g + 0.114 * color.b;
        FragColor = vec4(gray, gray, gray, color.a);
    }"#;

    let filter = OpenGLFrameFilter::new_simple(fragment_shader).unwrap();
    let frame_pipeline_builder: FramePipelineBuilder = AVMediaType::AVMEDIA_TYPE_VIDEO.into();
    let frame_pipeline_builder = frame_pipeline_builder.filter("opengl", Box::new(filter));

    FfmpegContext::builder()
        .input("input.mp4")
        .output(Output::from("output.mp4").add_frame_pipeline(frame_pipeline_builder))
        .build()?
        .start()?
        .wait()?;
    Ok(())
}

この例では、GPUを使って動画をグレースケールに変換し、リアルタイム処理に適しています。

高度な例2:音量調整フィルター(音声)

公式のcustom_volume_filterの例を参考に、音量調整フィルターを実装します:

実装コード

use ez_ffmpeg::{AVMediaType, Frame};
use ez_ffmpeg::core::filter::frame_filter::FrameFilter;
use ez_ffmpeg::filter::frame_filter_context::FrameFilterContext;

pub struct VolumeFilter {
    pub(crate) gain: f32,
}

impl FrameFilter for VolumeFilter {
    fn media_type(&self) -> AVMediaType {
        AVMediaType::AVMEDIA_TYPE_AUDIO
    }

    fn filter_frame(&mut self, mut frame: Frame, _ctx: &FrameFilterContext) -> Result<Option<Frame>, String> {
        if unsafe { frame.as_ptr().is_null() } {
            println!("終了フレームを受け取りました");
            return Ok(Some(frame));
        }
        // S16フォーマットの音声のみをサポート
        if unsafe { (*frame.as_ptr()).format } != 8 {
            return Err("サポートされていないサンプルフォーマット".to_string());
        }

        let data = plane_mut(&mut frame);
        let samples = unsafe { std::slice::from_raw_parts_mut(data.as_mut_ptr() as *mut i16, data.len() / 2) };
        for sample in samples.iter_mut() {
            let new_sample = (*sample as f32 * self.gain).clamp(-32768.0, 32767.0) as i16;
            *sample = new_sample;
        }
        Ok(Some(frame))
    }
}

#[inline]
pub fn plane_mut(frame: &mut Frame) -> &mut [u8] {
    unsafe {
        std::slice::from_raw_parts_mut(
            (*frame.as_mut_ptr()).data[0],
            (*frame.as_mut_ptr()).linesize[0] as usize * (*frame.as_mut_ptr()).sample_rate as usize,
        )
    }
}

完全な実行コード

use ez_ffmpeg::{AVMediaType, FfmpegContext, Output};
use ez_ffmpeg::filter::frame_pipeline_builder::FramePipelineBuilder;
use crate::volume_filter::VolumeFilter;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let frame_pipeline_builder: FramePipelineBuilder = AVMediaType::AVMEDIA_TYPE_AUDIO.into();
    let volume_filter = VolumeFilter { gain: 0.5 };
    let frame_pipeline_builder = frame_pipeline_builder.filter("volume", Box::new(volume_filter));

    FfmpegContext::builder()
        .input("test.mp4")
        .output(Output::from("output.mp4").add_frame_pipeline(frame_pipeline_builder))
        .build()?
        .start()?
        .wait()?;
    Ok(())
}

この例では、音声の音量を50%に下げ、音声処理シーンに適しています。


まとめと今後の展望

Rustとez-ffmpegを使えば、純粋なRustコードでFFmpegのカスタムフィルターを安全かつ効率的に実装できます。従来のC APIの複雑さとセキュリティ問題を解決し、YUV420の明るさ調整からGPUアクセラレーションによるグレースケールフィルター、音声の音量調整まで、ez-ffmpegは柔軟なソリューションを提供します。リアルタイム動画処理、機械学習のデータ拡張、ゲーム開発など、さまざまなシーンで活躍します。今後は、ハードウェアアクセラレーションによるエンコード/デコードやストリーミング処理などの機能もさらに探求できます。

もっと詳しく知りたい方は、ez-ffmpeg GitHubリポジトリをご覧ください。ドキュメントやサンプルコードが揃っています。

Discussion