30
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

2021年HUITアドベントカレンダーAdvent Calendar 2021

Day 5

【C#】FFmpeg APIで動画プレーヤーを1からつくってみる(前編)

Last updated at Posted at 2021-12-04

この記事はHUITアドベントカレンダー5日目の記事です。
大変複雑なため、記事が長くなりすぎてしまい、前後編に分けることになりました。後編はこちら

はじめに

この記事で扱うのはFFmpeg API、つまりDLLを叩くもので、

ffmpeg -i input.mp4 -c:v copy -c:a copy output.avi

みたいな 生ぬるい 話は一切でてきません。FFmpeg APIの雰囲気や動画プレーヤーのしくみを知りたい人向けです。

  • とりあえず動画を変換・解析してみたい! => ffmpeg.exe や ffprove.exe をコマンドラインで使いましょう。Process クラスが有用。
  • とりあえず動画を再生したい! => WPFには動画再生用のコントロールが標準であります。MediaElement で検索。
  • 動画のフレームの画素データがほしい! => OpenCVがおすすめです。OpenCVSharpというC#ラッパーがあるので簡単に扱えます。
  • リアルタイムで変換・デコードしたい! => ffmpeg.exe の出力をコンソールの標準出力にリダイレクトしましょう。 pipe:1 を出力パスの代わりに書きます。

といった具合に、たいていの用途では FFmpeg API を使うのはおすすめしません。
OpenCVは動画を扱う方法としては少し意外かもしれませんが、OpenCVSharp という高レベルのC#ラッパーがあるのがミソです。  
昔コンソールで「BadApple!」を再生するのに使いました(そのうち記事にするかもしれません)が、結構使いやすいです。おすすめ。

なぜこのようなことを書いたかといえば、FFmpeg APIは(C#では) かなり扱いにくい からです。

それでもFFmpeg APIが使いたい

かなり複雑なことをしたい場合、もうFFmpeg APIしか選択肢がないかもしれません。
FFmpeg が非常に高機能かつ高パフォーマンスなのは事実で、動画プレーヤーを1から作りたい、といった変わったケースではかなり有力です。
覚悟を決めてHello Worldしましょう。

幸いにも、大量の [DLLImport] を書く必要はありません。FFmpeg.AutoGen というライブラリを使います。
このライブラリは薄いラッパーで、[DLLImport] 相当のコードを自動生成したもののようです。
現状C#向けのラッパーはほとんどなく、検索してヒットするのは ffmpeg.exeの方のラッパー(?) だったりします。

薄いラッパーなので大量の * からは逃げられませんが、情報源が本当に少ないFFmpeg APIを扱う上でメソッドのシグネチャが何も変わっていないのは逆に利点です。
今のうちにufcppでunsafeの復習をしておきましょう
公式のサンプルコードやごくわずかな情報を活用しやすく、c++のコードがほぼコピペで動く場合もあります。特にこちらの記事には大変お世話になりました。

準備

まず先程のライブラリをNuGetで取ってきます。使用するFFmpegのバージョンに合わせたNuGetパッケージのバージョンを選択します。この記事では4.4.1を利用しています。
次にFFmpegのバイナリを用意します。自分でビルドしてもよいですが、ビルド済みのものをダウンロードするのが楽でしょう。
今回の動画プレーヤーはWindows向けなので、Windowsでの話になってしまいますが、公式サイトにビルド済みバイナリを配布している有志サイトへのリンクがあります。今回は gyan.dev さんの方のビルド済みバイナリを利用しました。

「ffmpeg-release-full-shared.7z」のように「shared」が入るものをダウンロードすると今回必要なDLLが手に入ります。ライセンスがGPLなので注意。
普段「FFmpeg」と呼んでいたものはいくつかのライブラリの集合体なのでした。ちなみにffmpeg.dllみたいなものが必要なわけではないです。以下が個別にあればOK。

DLLの名前 APIでの主な接頭辞 内容
avutil av_ メモリ管理などの便利機能
avformat avformat_ 動画コンテナに関わる処理
avcodec avcodec_ 動画・音声コーデックのエンコード・デコード
swscale sws_ 画像のスケーリング
swresample swr_ 音声のリサンプリング

となっております。今後全部使います。

ファイルの読み込み

初期化

予め using FFmpeg.AutoGen;しておいてください。以後省略します。
また、今後unsafeコードが大量に登場しますので予めunsafeを有効にしておいてください。

ffmpeg.RootPath = @"C:\Users\{ユーザー名}\Downloads\ffmpeg-4.4.1-full_build-shared\bin";
ffmpeg.av_register_all();

RootPath プロパティの値として、先程ダウンロードしたDLL群のあるディレクトリを指定します。
その上で、ffmpeg.av_register_all()を呼んでライブラリを初期化します。最初に1度だけ呼んであげる必要があります。
コンストラクタなどで呼んであげてください。

エラー処理定形コード

今後呼ぶ関数には、「失敗すると負のエラーコードが返り、成功すると0が返る」ものがたくさんあります。
例外に変換する拡張メソッドを予め書いておきましょう。今後当然のように登場します。

public static int OnError(this int n, Action act)
{
    if (n < 0)
    {
        var buffer = Marshal.AllocHGlobal(1000);
        string str;
        unsafe
        {
            ffmpeg.av_make_error_string((byte*)buffer.ToPointer(), 1000, n);
            str = new string((sbyte*)buffer.ToPointer());
        }
        Marshal.FreeHGlobal(buffer);
        Debug.WriteLine(str);
        act.Invoke();
    }
    return n;
}

av_make_error_string 関数は、エラーコードからエラーメッセージを取得する関数です。こちらでバッファを用意してあげて呼びます。
第一引数にバッファのポインタ、第二引数にバッファの長さ、第三引数にエラーコードを投げます。
バッファの長さは1000byteで決め打ちしてますが、このへんは適当に調整してください。

ファイルを開く

// クラスのフィールド
string path = "{有効な動画のパス}";
AVFormatContext* formatContext;

public void OpenFile(string path)
{
    AVFormatContext* _formatContext = null;
    ffmpeg.avformat_open_input(&_formatContext, path, null, null)
        .OnError(() => throw new InvalidOperationException("指定のファイルは開けませんでした。"));
    formatContext = _formatContext;

    ffmpeg.avformat_find_stream_info(formatContext, null)
        .OnError(() => throw new InvalidOperationException("ストリームを検出できませんでした。"));
}

まず、avformat_open_input 関数を呼びます。第一引数にAVFormatContextのダブルポインタを、第二引数に動画のパスを渡します。
いきなりダブルポインタですが頑張って理解してください。
これでAVFormatContext が取れましたので、次に動画に含まれるストリームを解析します。
avformat_find_stream_info関数を呼びます。第一引数にAVFormatContextのポインタを渡すと、渡したAVFormatContextに情報が書き込まれます。

ストリームを探す

private AVStream* GetFirstVideoStream()
{
    for (int i = 0; i < (int)formatContext->nb_streams; ++i)
    {
        var stream = formatContext->streams[i];
        if (stream->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
        {
            return stream;
        }
    }
    return null;
}

今後->演算子が頻出します。普段のC#では見ないし使わないと思うので今のうちに復習しておいてください。
AVFormatContextstreamフィールド(配列様)を漁ってストリームを探します。
ストリームには、動画ストリーム、音声ストリーム、字幕ストリームなどなど色々ありますので、目的のものが見つかるまでループを回します。
ストリームは複数ある可能性(例:日本語音声と英語音声)が考えられますが、今回は便宜上最初に見つかったストリームのみを有効とします。

上記のは動画ストリームを探すメソッドでしたが、音声ストリームを探すものも用意します。

private AVStream* GetFirstAudioStream()
{
    for (int i = 0; i < (int)formatContext->nb_streams; i++)
    {
        var stream = formatContext->streams[i];
        if (stream->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO)
        {
            return stream;
        }
    }
    return null;
}

コーデックを開く

ストリームを見つけたので、今度は各ストリームのコーデックに合わせたデコーダを初期化しましょう。

private AVStream* videoStream;
private AVCodec* videoCodec;
private AVCodecContext* videoCodecContext;

// OpenFile関数の続き
videoStream = GetFirstVideoStream();

if (videoStream is not null)
{
    videoCodec = ffmpeg.avcodec_find_decoder(videoStream->codecpar->codec_id);
    if (videoCodec is null)
    {
        throw new InvalidOperationException("必要な動画デコーダを検出できませんでした。");
    }

    videoCodecContext = ffmpeg.avcodec_alloc_context3(videoCodec);
    if (videoCodecContext is null)
    {
        throw new InvalidOperationException("動画コーデックのCodecContextの確保に失敗しました。");
    }
    ffmpeg.avcodec_parameters_to_context(videoCodecContext, videoStream->codecpar)
        .OnError(() => throw new InvalidOperationException("動画コーデックパラメータの設定に失敗しました。"));
    ffmpeg.avcodec_open2(videoCodecContext, videoCodec, null)
        .OnError(() => throw new InvalidOperationException("動画コーデックの初期化に失敗しました。"));
}

avcodec_find_decoder 関数でデコーダを取得します。今回はストリームのコーデック情報から必要なデコーダを自動検出しています。
例えばHWデコーダを使いたいなど、使用するデコーダをカスタマイズする場合はここでデコーダを変えます。
こちらの関数は戻り値としてデコーダが得られます。

この先は定形コードです。説明するよりコードを見てもらうほうが理解が早いと思います。
AVCodecContextを経由してAVCodecを初期化すれば準備完了です。

音声コーデックも開きます。やることは同じです。以下のコード全体像で把握してください。

完成形

ここまでの処理を1つのクラスにまとめたものを示します。
最初はコピペでも構いませんが何となく何をしているかは把握してください。

/// <summary>
/// 動画デコーダを表現します。
/// </summary>
public unsafe class Decoder : IDisposable
{
    public Decoder()
    {
        ffmpeg.RootPath = @"{DLLがあるディレクトリのパス}";
        ffmpeg.av_register_all();
    }

    private AVFormatContext* formatContext;
    /// <summary>
    /// 現在の <see cref="AVFormatContext"/> を取得します。
    /// </summary>
    public AVFormatContext FormatContext { get => *formatContext; }

    private AVStream* videoStream;
    /// <summary>
    /// 現在の動画ストリームを表す <see cref="AVStream"/> を取得します。
    /// </summary>
    public AVStream VideoStream { get => *videoStream; }

    private AVStream* audioStream;

    private AVCodec* videoCodec;
    /// <summary>
    /// 現在の動画コーデックを表す <see cref="AVCodec"/> を取得します。
    /// </summary>
    public AVCodec VideoCodec { get => *videoCodec; }

    private AVCodec* audioCodec;

    private AVCodecContext* videoCodecContext;
    /// <summary>
    /// 現在の動画コーデックの <see cref="AVCodecContext"/> を取得します。
    /// </summary>
    public AVCodecContext VideoCodecContext { get => *videoCodecContext; }

    private AVCodecContext* audioCodecContext;

    /// <summary>
    /// ファイルを開き、デコーダを初期化します。
    /// </summary>
    /// <param name="path">開くファイルのパス。</param>
    /// <exception cref="InvalidOperationException" />
    public void OpenFile(string path)
    {
        AVFormatContext* _formatContext = null;
        ffmpeg.avformat_open_input(&_formatContext, path, null, null)
            .OnError(() => throw new InvalidOperationException("指定のファイルは開けませんでした。"));
        formatContext = _formatContext;

        ffmpeg.avformat_find_stream_info(formatContext, null)
            .OnError(() => throw new InvalidOperationException("ストリームを検出できませんでした。"));

        videoStream = GetFirstVideoStream();
        audioStream = GetFirstAudioStream();

        if (videoStream is not null)
        {
            videoCodec = ffmpeg.avcodec_find_decoder(videoStream->codecpar->codec_id);
            if (videoCodec is null)
            {
                throw new InvalidOperationException("必要な動画デコーダを検出できませんでした。");
            }

            videoCodecContext = ffmpeg.avcodec_alloc_context3(videoCodec);
            if (videoCodecContext is null)
            {
                throw new InvalidOperationException("動画コーデックのCodecContextの確保に失敗しました。");
            }
            ffmpeg.avcodec_parameters_to_context(videoCodecContext, videoStream->codecpar)
                .OnError(() => throw new InvalidOperationException("動画コーデックパラメータの設定に失敗しました。"));
            ffmpeg.avcodec_open2(videoCodecContext, videoCodec, null)
                .OnError(() => throw new InvalidOperationException("動画コーデックの初期化に失敗しました。"));
        }
        if (audioStream is not null)
        {
            audioCodec = ffmpeg.avcodec_find_decoder(audioStream->codecpar->codec_id);
            if (audioCodec is null)
            {
                throw new InvalidOperationException("必要な音声デコーダを検出できませんでした。");
            }

            audioCodecContext = ffmpeg.avcodec_alloc_context3(audioCodec);
            if (audioCodecContext is null)
            {
                throw new InvalidOperationException("音声コーデックのCodecContextの確保に失敗しました。");
            }
            ffmpeg.avcodec_parameters_to_context(audioCodecContext, audioStream->codecpar)
                .OnError(() => throw new InvalidOperationException("音声コーデックのパラメータ設定に失敗しました。"));
            ffmpeg.avcodec_open2(audioCodecContext, audioCodec, null)
                .OnError(() => throw new InvalidOperationException("音声コーデックの初期化に失敗しました。"));
        }
    }

    private AVStream* GetFirstVideoStream()
    {
        for (int i = 0; i < (int)formatContext->nb_streams; ++i)
        {
            var stream = formatContext->streams[i];
            if (stream->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
            {
                return stream;
            }
        }
        return null;
    }

    private AVStream* GetFirstAudioStream()
    {
        for (int i = 0; i < (int)formatContext->nb_streams; i++)
        {
            var stream = formatContext->streams[i];
            if (stream->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO)
            {
                return stream;
            }
        }
        return null;
    }
}

お疲れさまでした。これでファイルを開く処理は完了です。一回休憩しましょう。

ファイルを開くだけでも結構たいへんです。「はじめに」のセッションでくどいことを書いた理由がおわかりいただけたと思います...
FFmpeg APIを使ってみたくてこの記事にたどりついた方はこのあたりでスっとタブを閉じていることでしょう

デコードする

パケットの読み出しと配送

まずは、動画ファイルから「パケット」を取り出します。
パケットは、どれかのストリームに関連するデータです。目的のストリームではないかもしれません。
av_read_frame関数が0を返した場合、パケットが取得できています。0でない場合は、動画の末端に達しています。

AVPacket packet = new AVPacket();
var result = ffmpeg.av_read_frame(formatContext, &packet);

パケットのstream_indexをみて、正しいデコーダにパケットを配送します。
パケットは、参照カウンタ方式で管理されているので使い終わったら参照を外してください。

if (result == 0)
{
    if (packet.stream_index == videoStream->index)
    {
        if (packet.stream_index == index)
        {
            ffmpeg.avcodec_send_packet(videoCodecContext, &packet)
            .OnError(() => throw new InvalidOperationException("動画デコーダへのパケットの送信に失敗しました。"));
            ffmpeg.av_packet_unref(&packet);
        }
    }
}

フレームの取得

パケットを送信したら、avcodec_receive_frame 関数を呼びフレームの読み取りを試みます。
パケットとフレームは対応していないので、一回のパケット送信操作で複数のフレームが得られたり、あるいは何も得られなかったりしますので、そのことを念頭に置いて設計してください(後ほど例を示します)。また、av_frame_allocしたら必ず責任を持ってav_frame_freeする必要があります。
このフレームがデコードの結果得られる(ほしい)ものです。サイズが大きいので配列などに突っ込むのはおすすめしません。
必要に応じて(メソッドを呼び出したら)返すように設計しましょう。

AVFrame* frame = ffmpeg.av_frame_alloc();
ffmpeg.avcodec_receive_frame(videoCodecContext, frame)

動画のフレームは非常にサイズが大きく、解放が漏れた場合大変なことになります(あっという間にメモリを使い果たす)。
解放漏れを起こさないよう注意するとともに、適宜マネージドラッパーを作成するなど保険をかけましょう。このあたりの実装例は後ほど示します。

実際解放漏れがあったときは、数十秒以内に私のPCの16GBのメモリを使い切り落ちました。本当に注意してください。

終端に達したとき

最後に、パケットが読み取れなくなり、動画の終端に達した場合は各デコーダにnull を送信して終端に達したことを通達します。
逆にプログラムのバグなどで誤ってnullを途中送信してしまうと、その次のパケット送信操作で「すでに動画の終端に達しています(筆者訳)」というエラーが起きます。
一見何が間違っているのかわかりにくいので注意しましょう(一敗)。

ffmpeg.avcodec_send_packet(videoCodecContext, null)
                .OnError(() => throw new InvalidOperationException("デコーダへのnullパケットの送信に失敗しました。"));

null を送信したあと、デコーダに残っているフレームを最後まで読み取りきるのを忘れないようにしてください。

解放

AVCodecContextAVFormatContext は解放が必要です。avcodec_free_context あるいは avformat_close_input 関数で最後に解放してください。

デコードに必要な実装

パケットのキュー

動画プレーヤーのように動画と音声を同時に扱う場合、取得したパケットをキューする機構が必要です。
デコーダからフレームを読み出すことを試みる前に次のパケットを送信することは許されていないため、動画パケットを探している途中で音声パケットが出てくるなどした場合キューに入れます。

実は、パケットはユーザーコード側に所有権がありません。C#コード側で保持しておいても普通に解放されます。
したがって、パケットを自分で複製した上でそれを保持しなければなりません。(これに辿り着くのに数日かかりました。本当に情報が少ない。)
C#ではポインタをジェネリック型引数にできないのでAVPacketPtr構造体でラップしています。

private struct AVPacketPtr
{
    public AVPacket* Ptr;
    public AVPacketPtr(AVPacket* ptr)
    {
        Ptr = ptr;
    }
}

private Queue<AVPacketPtr> videoPackets = new();
private Queue<AVPacketPtr> audioPackets = new();

AVPacket packet = new AVPacket();
var result = ffmpeg.av_read_frame(formatContext, &packet);
if (result == 0)
{
    if (packet.stream_index == videoStream->index)
    {
        if (packet.stream_index == index)
        {
            ffmpeg.avcodec_send_packet(videoCodecContext, &
            .OnError(() => throw new InvalidOperationExcept
            ffmpeg.av_packet_unref(&packet);
            return 0;
        }
        else
        {
            var _packet = ffmpeg.av_packet_clone(&packet);
            videoPackets.Enqueue(new AVPacketPtr(_packet));
        }
    }
}

フレームの管理

デコード処理の成果物はフレームで、今後はこれを扱いますが、前述の通り解放漏れを起こすと悲惨です。
よってこのフレームのラッパーを作り、上位のコードにはラップされたフレームを渡すことにします。
IDisposableインターフェイスを実装し、さらにファイナライザ(デストラクタ)を定義しそこから解放ロジックを呼びます。
これでusingなどの確実な解放を保証する言語機能が使えますし、最悪GCが解放してくれます。

/// <summary>
/// ラップされた <see cref="AVFrame"/> を表現します。解放漏れを防止します。
/// </summary>
public unsafe class ManagedFrame : IDisposable
{
    public ManagedFrame(AVFrame* frame)
    {
        this.frame = frame;
    }

    private readonly AVFrame* frame;

    /// <summary>
    /// ラップされたフレームを取得します。このフレームを独自に解放しないでください。
    /// </summary>
    public AVFrame* Frame { get => frame; }

    ~ManagedFrame()
    {
        DisposeUnManaged();
    }

    /// <inheritdoc />
    public void Dispose()
    {
        DisposeUnManaged();
        GC.SuppressFinalize(this);
    }

    private bool isDisposed = false;

    private void DisposeUnManaged()
    {
        if (isDisposed) { return; }

        AVFrame* aVFrame = frame;
        ffmpeg.av_frame_free(&aVFrame);

        isDisposed = true;
    }
}

完成したデコーダーのコード

decoder.cs
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

using FFmpeg.AutoGen;

namespace FFmpegWraper
{
    /// <summary>
    /// 動画デコーダを表現します。
    /// </summary>
    public unsafe class Decoder : IDisposable
    {
        public Decoder()
        {
            ffmpeg.RootPath = @"{DLLのあるディレクトリのパス}";
            ffmpeg.av_register_all();
        }

        private AVFormatContext* formatContext;
        /// <summary>
        /// 現在の <see cref="AVFormatContext"/> を取得します。
        /// </summary>
        public AVFormatContext FormatContext { get => *formatContext; }

        private AVStream* videoStream;
        /// <summary>
        /// 現在の動画ストリームを表す <see cref="AVStream"/> を取得します。
        /// </summary>
        public AVStream VideoStream { get => *videoStream; }

        private AVStream* audioStream;

        private AVCodec* videoCodec;
        /// <summary>
        /// 現在の動画コーデックを表す <see cref="AVCodec"/> を取得します。
        /// </summary>
        public AVCodec VideoCodec { get => *videoCodec; }

        private AVCodec* audioCodec;

        private AVCodecContext* videoCodecContext;
        /// <summary>
        /// 現在の動画コーデックの <see cref="AVCodecContext"/> を取得します。
        /// </summary>
        public AVCodecContext VideoCodecContext { get => *videoCodecContext; }

        private AVCodecContext* audioCodecContext;

        /// <summary>
        /// ファイルを開き、デコーダを初期化します。
        /// </summary>
        /// <param name="path">開くファイルのパス。</param>
        /// <exception cref="InvalidOperationException" />
        public void OpenFile(string path)
        {
            AVFormatContext* _formatContext = null;
            ffmpeg.avformat_open_input(&_formatContext, path, null, null)
                .OnError(() => throw new InvalidOperationException("指定のファイルは開けませんでした。"));
            formatContext = _formatContext;

            ffmpeg.avformat_find_stream_info(formatContext, null)
                .OnError(() => throw new InvalidOperationException("ストリームを検出できませんでした。"));

            videoStream = GetFirstVideoStream();
            audioStream = GetFirstAudioStream();

            if (videoStream is not null)
            {
                videoCodec = ffmpeg.avcodec_find_decoder(videoStream->codecpar->codec_id);
                if (videoCodec is null)
                {
                    throw new InvalidOperationException("必要な動画デコーダを検出できませんでした。");
                }

                videoCodecContext = ffmpeg.avcodec_alloc_context3(videoCodec);
                if (videoCodecContext is null)
                {
                    throw new InvalidOperationException("動画コーデックのCodecContextの確保に失敗しました。");
                }
                ffmpeg.avcodec_parameters_to_context(videoCodecContext, videoStream->codecpar)
                    .OnError(() => throw new InvalidOperationException("動画コーデックパラメータの設定に失敗しました。"));
                ffmpeg.avcodec_open2(videoCodecContext, videoCodec, null)
                    .OnError(() => throw new InvalidOperationException("動画コーデックの初期化に失敗しました。"));
            }
            if (audioStream is not null)
            {
                audioCodec = ffmpeg.avcodec_find_decoder(audioStream->codecpar->codec_id);
                if (audioCodec is null)
                {
                    throw new InvalidOperationException("必要な音声デコーダを検出できませんでした。");
                }

                audioCodecContext = ffmpeg.avcodec_alloc_context3(audioCodec);
                if (audioCodecContext is null)
                {
                    throw new InvalidOperationException("音声コーデックのCodecContextの確保に失敗しました。");
                }
                ffmpeg.avcodec_parameters_to_context(audioCodecContext, audioStream->codecpar)
                    .OnError(() => throw new InvalidOperationException("音声コーデックのパラメータ設定に失敗しました。"));
                ffmpeg.avcodec_open2(audioCodecContext, audioCodec, null)
                    .OnError(() => throw new InvalidOperationException("音声コーデックの初期化に失敗しました。"));
            }
        }

        private AVStream* GetFirstVideoStream()
        {
            for (int i = 0; i < (int)formatContext->nb_streams; ++i)
            {
                var stream = formatContext->streams[i];
                if (stream->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_VIDEO)
                {
                    return stream;
                }
            }
            return null;
        }

        private AVStream* GetFirstAudioStream()
        {
            for (int i = 0; i < (int)formatContext->nb_streams; i++)
            {
                var stream = formatContext->streams[i];
                if (stream->codecpar->codec_type == AVMediaType.AVMEDIA_TYPE_AUDIO)
                {
                    return stream;
                }
            }
            return null;
        }

        private struct AVPacketPtr
        {
            public AVPacket* Ptr;
            public AVPacketPtr(AVPacket* ptr)
            {
                Ptr = ptr;
            }
        }

        private object sendPackedSyncObject = new();

        private Queue<AVPacketPtr> videoPackets = new();
        private Queue<AVPacketPtr> audioPackets = new();

        public int SendPacket(int index)
        {
            lock (sendPackedSyncObject)
            {
                if (index == videoStream->index)
                {
                    if (videoPackets.TryDequeue(out var ptr))
                    {
                        ffmpeg.avcodec_send_packet(videoCodecContext, ptr.Ptr)
                        .OnError(() => throw new InvalidOperationException("動画デコーダへのパケットの送信に失敗しました。"));
                        ffmpeg.av_packet_unref(ptr.Ptr);
                        return 0;
                    }
                }
                if (index == audioStream->index)
                {
                    if (audioPackets.TryDequeue(out var ptr))
                    {
                        ffmpeg.avcodec_send_packet(audioCodecContext, ptr.Ptr)
                        .OnError(() => throw new InvalidOperationException("音声デコーダへのパケットの送信に失敗しました。"));
                        ffmpeg.av_packet_unref(ptr.Ptr);
                        return 0;
                    }
                }

                while (true)
                {
                    AVPacket packet = new AVPacket();
                    var result = ffmpeg.av_read_frame(formatContext, &packet);
                    if (result == 0)
                    {
                        if (packet.stream_index == videoStream->index)
                        {
                            if (packet.stream_index == index)
                            {
                                ffmpeg.avcodec_send_packet(videoCodecContext, &packet)
                                .OnError(() => throw new InvalidOperationException("動画デコーダへのパケットの送信に失敗しました。"));
                                ffmpeg.av_packet_unref(&packet);
                                return 0;
                            }
                            else
                            {
                                var _packet = ffmpeg.av_packet_clone(&packet);
                                videoPackets.Enqueue(new AVPacketPtr(_packet));
                                continue;
                            }
                        }
                        if (packet.stream_index == audioStream->index)
                        {
                            if (packet.stream_index == index)
                            {
                                ffmpeg.avcodec_send_packet(audioCodecContext, &packet)
                                .OnError(() => throw new InvalidOperationException("音声デコーダへのパケットの送信に失敗しました。"));
                                ffmpeg.av_packet_unref(&packet);
                                return 0;
                            }
                            else
                            {
                                var _packet = ffmpeg.av_packet_clone(&packet);
                                audioPackets.Enqueue(new AVPacketPtr(_packet));
                                continue;
                            }
                        }
                    }
                    else
                    {
                        return -1;
                    }
                }
            }
        }

        /// <summary>
        /// 次のフレームを読み取ります。動画の終端に達している場合は <c>null</c> が返されます。
        /// </summary>
        public unsafe ManagedFrame ReadFrame()
        {
            var frame = ReadUnsafeFrame();
            if (frame is null)
            {
                return null;
            }
            return new ManagedFrame(frame);
        }

        private bool isVideoFrameEnded;

        /// <summary>
        /// 次のフレームを読み取ります。動画の終端に達している場合は <c>null</c> が返されます。
        /// </summary>
        /// <remarks>
        /// 取得したフレームは <see cref="ffmpeg.av_frame_free(AVFrame**)"/> を呼び出して手動で解放する必要があることに注意してください。
        /// </remarks>
        /// <returns></returns>
        public unsafe AVFrame* ReadUnsafeFrame()
        {
            AVFrame* frame = ffmpeg.av_frame_alloc();

            if (ffmpeg.avcodec_receive_frame(videoCodecContext, frame) == 0)
            {
                return frame;
            }
            if (isVideoFrameEnded)
            {
                return null;
            }

            int n;
            while ((n = SendPacket(videoStream->index)) == 0)
            {
                if (ffmpeg.avcodec_receive_frame(videoCodecContext, frame) == 0)
                {
                    return frame;
                }
                else
                {

                }
            }

            isVideoFrameEnded = true;
            ffmpeg.avcodec_send_packet(videoCodecContext, null)
                .OnError(() => throw new InvalidOperationException("デコーダへのnullパケットの送信に失敗しました。"));
            if (ffmpeg.avcodec_receive_frame(videoCodecContext, frame) == 0)
            {
                return frame;
            }
            return null;
        }

        /// <summary>
        /// 次の音声フレームを読み取ります。動画の終端に達している場合は <c>null</c> が返されます。
        /// </summary>
        public unsafe ManagedFrame ReadAudioFrame()
        {
            var frame = ReadUnsafeAudioFrame();
            if (frame is null)
            {
                return null;
            }
            return new ManagedFrame(frame);
        }

        private bool isAudioFrameEnded;

        /// <summary>
        /// 次の音声フレームを読み取ります。動画の終端に達している場合は <c>null</c> が返されます。
        /// </summary>
        /// <remarks>
        /// 取得したフレームは <see cref="ffmpeg.av_frame_free(AVFrame**)"/> を呼び出して手動で解放する必要があることに注意してください。
        /// </remarks>
        /// <returns></returns>
        public unsafe AVFrame* ReadUnsafeAudioFrame()
        {
            AVFrame* frame = ffmpeg.av_frame_alloc();

            if (ffmpeg.avcodec_receive_frame(audioCodecContext, frame) == 0)
            {
                return frame;
            }
            if (isAudioFrameEnded)
            {
                return null;
            }

            while (SendPacket(audioStream->index) == 0)
            {
                if (ffmpeg.avcodec_receive_frame(audioCodecContext, frame) == 0)
                {
                    return frame;
                }
            }

            isAudioFrameEnded = true;
            ffmpeg.avcodec_send_packet(audioCodecContext, null)
                .OnError(() => throw new InvalidOperationException("デコーダへのnullパケットの送信に失敗しました。"));
            if (ffmpeg.avcodec_receive_frame(audioCodecContext, frame) == 0)
            {
                return frame;
            }
            return null;
        }

        ~Decoder()
        {
            DisposeUnManaged();
        }

        /// <inheritdoc />
        public void Dispose()
        {
            DisposeUnManaged();
            GC.SuppressFinalize(this);
        }

        private bool isDisposed = false;
        private void DisposeUnManaged()
        {
            if (isDisposed) { return; }

            AVCodecContext* codecContext = videoCodecContext;
            AVFormatContext* formatContext = this.formatContext;

            ffmpeg.avcodec_free_context(&codecContext);
            ffmpeg.avformat_close_input(&formatContext);

            isDisposed = true;
        }
    }

    internal static class WrapperHelper
    {
        public static int OnError(this int n, Action act)
        {
            if (n < 0)
            {
                var buffer = Marshal.AllocHGlobal(1000);
                string str;
                unsafe
                {
                    ffmpeg.av_make_error_string((byte*)buffer.ToPointer(), 1000, n);
                    str = new string((sbyte*)buffer.ToPointer());
                }
                Marshal.FreeHGlobal(buffer);
                Debug.WriteLine(str);
                act.Invoke();
            }
            return n;
        }
    }
}
ManagedFrame.cs
using FFmpeg.AutoGen;

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace FFmpegWraper
{
    /// <summary>
    /// ラップされた <see cref="AVFrame"/> を表現します。解放漏れを防止します。
    /// </summary>
    public unsafe class ManagedFrame : IDisposable
    {
        public ManagedFrame(AVFrame* frame)
        {
            this.frame = frame;
        }

        private readonly AVFrame* frame;

        /// <summary>
        /// ラップされたフレームを取得します。このフレームを独自に解放しないでください。
        /// </summary>
        public AVFrame* Frame { get => frame; }

        ~ManagedFrame()
        {
            DisposeUnManaged();
        }

        /// <inheritdoc />
        public void Dispose()
        {
            DisposeUnManaged();
            GC.SuppressFinalize(this);
        }

        private bool isDisposed = false;

        private void DisposeUnManaged()
        {
            if (isDisposed) { return; }

            AVFrame* aVFrame = frame;
            ffmpeg.av_frame_free(&aVFrame);

            isDisposed = true;
        }
    }
}

まだデコードするだけなのに合わせて実に430行です。大変。
長くなりすぎてしまい、またエディタが重いので前編・後編に分けることにします。
後編へ続く。

30
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?