woonizzooni

FFmpeg Visual Studio 2019 컴파일-2 (동영상 플레이어, C++/MFC, C#/WPF) 본문

FFmpeg

FFmpeg Visual Studio 2019 컴파일-2 (동영상 플레이어, C++/MFC, C#/WPF)

woonizzooni 2019. 7. 24. 06:16

 

o 전제 사항 / 필요 조건

  - FFmpeg 사전지식 (이해하려면, 이해없이 헬로월드만 찍어봐도 됨)

  - Windows용 FFmpeg라이브러리 : 아래 둘 중 하나 선택

     1) 직접 빌드한 Windows용 FFmpeg 라이브러리 ('여기'참고)
     2) FFmpeg소스 빌드를 희망하지 않을 경우. (FFmpeg 소스 수정 불가) 

  - Visual Studio 20xx

  - mp4/mkv등의 동영상 파일 

 

 

o 목표

  - FFmpeg라이브러리와 연동되는 윈도우앱 빌드/실행

 


[MFC]

4년 전에 MFC로 작성된 문제 있는 앱 디버깅 경험(?)을 믿고 MFC로 일단 시작.

 

 

1. 프로젝트 만들기 

  > Visual Studio실행 > 새 프로젝트 만들기 > C++ 콘솔 앱 선택 후 다음 

  > 솔루션 이름 & 프로젝트 이름 설정 입력 후 만들기

2. 실행환경 설정

  > 구성속성 > 디버깅 > 환경 : ffmpeg.lib파일 경로 입력

    ex) PATH=%PATH%;C:\msys64\home\....\INSTALLED\bin : 

  > 구성속성 > VC++디렉토리 : FFmpeg 헤더파일 (*.h) 및 라이브러리 디렉토리 설정

    > 포함 디렉터리 : 

      $(VC_IncludePath);$(WindowsSDK_IncludePath);C:\msys64\home\...\INSTALLED\include;

    > 라이브러리 디렉터리 : 

      $(VC_LibraryPath_x86);$(WindowsSDK_LibraryPath_x86);$(NETFXKitsDir)Lib\um\x86

      ;C:\msys64\home\...\INSTALLED\bin;

 

3. 연동 코드 작성 : 프로젝트명.cpp파일에 아래 코드 붙여넣기. 

  > 디버그 로그레벨 & 동영상 파일이 제대로 열리고 코덱이 선택되어 열리는지 확인

 

extern "C" {
#include <libavformat/avformat.h>
#include <libavformat/avformat.h> 
#include <libavcodec/avcodec.h>
#include <libswresample/swresample.h>
#include <libswscale/swscale.h>
#include <libavutil/opt.h>
}
#include <iostream>

///> Library Link On Windows System
#pragma comment(lib,"avformat.lib")
#pragma comment(lib,"avcodec.lib")
#pragma comment(lib,"swresample.lib")
#pragma comment(lib,"swscale.lib")
#pragma comment(lib,"avutil.lib")
#pragma comment( lib, "avformat.lib" )
#pragma comment( lib, "avutil.lib" )

int main()
{
	av_log_set_level(AV_LOG_DEBUG);
	av_log(NULL, AV_LOG_INFO, "Hello world\n");

	AVFormatContext* context = NULL;

	int ret = avformat_open_input(&context, "F:\\Movies\\xx.mp4", NULL, NULL);
	if (ret != 0)
	{
		av_log(NULL, AV_LOG_ERROR, "avformat_open_input() failed\n");
		exit(-1);
	}

	ret = avformat_find_stream_info(context, NULL);
	if (ret < 0) {
		av_log(NULL, AV_LOG_ERROR, "Fail to get Stream Inform\n");
		exit(-1);
	}

	AVCodec *vCodec, *aCodec;
	AVCodecContext *vCodecContext, *aCodecContext;
	int vIndex = av_find_best_stream(context, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0);
	int aIndex = av_find_best_stream(context, AVMEDIA_TYPE_AUDIO, -1, vIndex, NULL, 0);

	if (vIndex >= 0) {
		vCodec = avcodec_find_decoder(context->streams[vIndex]->codecpar->codec_id);
		if (vCodec == NULL)
		{
			av_log(NULL, AV_LOG_ERROR, "avcodec_find_decoder(vcodec) failed.\n");
			exit(-1);
		}

		vCodecContext = avcodec_alloc_context3(vCodec);
		if (avcodec_open2(vCodecContext, vCodec, NULL) < 0)
		{
			av_log(NULL, AV_LOG_ERROR, "avcodec_open2(vcodec) failed.\n");
			exit(-1);
		}
	}

	if (aIndex >= 0) {
		aCodec = avcodec_find_decoder(context->streams[aIndex]->codecpar->codec_id);
		if (aCodec == NULL)
		{
			av_log(NULL, AV_LOG_ERROR, "avcodec_find_decoder(acodec) failed.\n");
			exit(-1);
		}
		aCodecContext = avcodec_alloc_context3(aCodec);

		if (avcodec_open2(aCodecContext, aCodec, NULL) < 0)
		{
			av_log(NULL, AV_LOG_ERROR, "avcodec_open2(acodec) failed.\n");
			exit(-1);
		}
	}

	avformat_close_input(&context);
	return 0;
}

 

4. 실행 결과

  > 로그 출력 확인. 

 

이 상태에서 MFC작업을 하려니 머리 텅텅? 다시 공부해야 하네?

그래! 차라리 안해본 WPF로 가보자!!

 

[기타 삽질 이력 (위 내용에 포함되진 않음)]

> _cplusplus매크로관련

   https://docs.microsoft.com/ko-kr/cpp/build/reference/zc-cplusplus?view=vs-2019

 

> #define __STDC_CONSTANT_MACROS

> adxwin.h가 없을 경우 : MFC 모듈이 설치되지 않았을 때.

   C++를 사용한 데스크톱 개발이 선택되었더라도 개별 구성요소에서 MFC는 미선택되어 있음 -_-
   (x86 및 x64용 Visual C++ MFC)

> ... 기억이 안나네.. 리눅스 짱짱맨! 


[WPF (Windows Presentation Foundation]

 

 

1. 프로젝트 만들기 (그림으로 대체한다)

 

2. FFmpeg 링크 환경 구성

  > FFmpeg폴더 생성 > 내부에 lib폴더 생성 후 빌드한 dll파일을 복사 & 붙여넣기.

  > NuGet 패키지 관리자에서 FFmpeg.AutoGen 설치

 

 

3. 연동 코드 작성 : 이미지 출력까지

  - 대상 파일 : 아래 6개 파일. (FFmpeg/밑의 파일은 생성 필요)

    MainWindow.xaml
    MainWIndow.xaml.cs
    FFmpeg/
        BinariesHelper.cs
        Helper.cs
        StreamDecoder.cs
        VideoFrameConverter.cs

 

    MainWindow.xaml : W x H 대충 입력, Image와 button추가(플레이)

<Window x:Class="MyMediaPlayer.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:MyMediaPlayer"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="800"
        Closing="MainWindow_Closing">
    <Grid>
        <Image x:Name="image" HorizontalAlignment="Left"
                Height="360" Margin="10,70,0,0" VerticalAlignment="Top" Width="720"/>
        <Button x:Name="Play_Button" Content="Play"
                HorizontalAlignment="Right" Margin="0,10,10,0" 
                VerticalAlignment="Top" Width="75" Height="55" Click="Play_Button_Click"/>
    </Grid>
</Window>

    MainWIndow.xaml.cs : url을 본인 환경에 맞는 파일로 변경

using System;
using System.IO;
using System.Linq;
using System.Drawing;
using System.Threading;
using System.Windows;
using System.Windows.Media.Imaging;
using System.Windows.Threading;
using System.Collections.Concurrent;
using System.Threading.Tasks;
using FFmpeg.AutoGen;
using MyMediaPlayer.FFmpeg;

namespace MyMediaPlayer
{
    public partial class MainWindow : Window
    {
        Thread videoThread;
        Dispatcher dispatcher = Application.Current.Dispatcher;

        private bool stopThread = false;

        public MainWindow()
        {
            InitializeComponent();

            BinariesHelper.RegisterFFmpegBinaries();
            videoThread = new Thread(new ThreadStart(PlayingMedia));
        }

        ConcurrentQueue<AVFrame> vq = new ConcurrentQueue<AVFrame>();
        ConcurrentQueue<AVFrame> aq = new ConcurrentQueue<AVFrame>();
        VideoFrameConverter vfc;

        private unsafe void PlayingMedia()
        {
            string url = @"D:\Movies\xxxx.mkv";

            using (var sd = new StreamDecoder(url))
            {
                Task tVideoTask = Task.Factory.StartNew(() => VideoTask(sd.vcodecContext));

                var sourceSize = sd.FrameSize;
                var sourcePixelFormat = sd.PixelFormat;
                var destinationSize = sourceSize;
                var destinationPixelFormat = AVPixelFormat.AV_PIX_FMT_BGR24;

                vfc = new VideoFrameConverter(sourceSize, sourcePixelFormat, destinationSize, destinationPixelFormat);
                var frameNumber = 0;
                while (sd.TryDecodeNextFrame(out var frame) && !stopThread)
                {

                    vq.Enqueue(frame);

                    frameNumber++;
                    if (frameNumber % 45 == 0)
                    {
                        System.Threading.Thread.Sleep(1000);
                        frameNumber = 0;
                    }
                }
            }
        }

        private unsafe void VideoTask(AVCodecContext* vcodecContext)
        {
            while (true)
            {
                if (vq.TryDequeue(out var frame))
                {
                    var convertedFrame = vfc.Convert(frame);

                    Bitmap bitmap = new Bitmap(
                        convertedFrame.width,
                        convertedFrame.height, 
                        convertedFrame.linesize[0], 
                        System.Drawing.Imaging.PixelFormat.Format24bppRgb,
                        (IntPtr)convertedFrame.data[0]);
                    BitmapToImageSource(bitmap);
                }
            }
        }

        void BitmapToImageSource(Bitmap bitmap)
        {
            dispatcher.BeginInvoke((Action)(() =>
            {
                using (MemoryStream memory = new MemoryStream())
                {
                    if (videoThread.IsAlive)
                    {
                        bitmap.Save(memory, System.Drawing.Imaging.ImageFormat.Bmp);
                        memory.Position = 0;

                        BitmapImage bitmapimage = new BitmapImage();
                        bitmapimage.BeginInit();
                        bitmapimage.CacheOption = BitmapCacheOption.OnLoad;
                        bitmapimage.StreamSource = memory;
                        bitmapimage.EndInit();

                        image.Source = bitmapimage;
                    }
                }
            }));

        }

        private void Play_Button_Click(object sender, RoutedEventArgs e)
        {
            if (videoThread.ThreadState == System.Threading.ThreadState.Unstarted)
            {
                videoThread.Start();
            }
        }

        private void MainWindow_Closing(object sender, System.ComponentModel.CancelEventArgs e)
        {
            if (videoThread.IsAlive)
            {
                stopThread = true;
                videoThread.Join();
            }
        }
    }
}

 

    FFmpeg/

        BinariesHelper.cs

using FFmpeg.AutoGen;
using System;
using System.IO;
using System.Runtime.InteropServices;

namespace MyMediaPlayer.FFmpeg
{
    public class BinariesHelper
    {
        internal static void RegisterFFmpegBinaries()
        {
            var current = Environment.CurrentDirectory;
            //var probe = Path.Combine("FFmpeg", "lib", Environment.Is64BitProcess ? "x64" : "x86");
            var probe = Path.Combine("FFmpeg", "lib");
            while (current != null)
            {
                var ffmpegBinaryPath = Path.Combine(current, probe);
                if (Directory.Exists(ffmpegBinaryPath))
                {
                    ffmpeg.RootPath = ffmpegBinaryPath;
                    return;
                }
                current = Directory.GetParent(current)?.FullName;
            }

        }
    }
}

        Helper.cs

using System;
using System.Runtime.InteropServices;
using FFmpeg.AutoGen;

namespace MyMediaPlayer.FFmpeg
{
    internal static class Helper
    {
        public static unsafe string av_strerror(int error)
        {
            var bufferSize = 1024;
            var buffer = stackalloc byte[bufferSize];
            ffmpeg.av_strerror(error, buffer, (ulong)bufferSize);
            var message = Marshal.PtrToStringAnsi((IntPtr)buffer);
            return message;
        }

        public static int ThrowExceptionIfError(this int error)
        {
            if (error < 0) throw new ApplicationException(av_strerror(error));
            return error;
        }
    }
}

        StreamDecoder.cs

using FFmpeg.AutoGen;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace MyMediaPlayer.FFmpeg
{
    public sealed unsafe class StreamDecoder : IDisposable
    {
        private readonly AVFormatContext* _pFormatContext;
        private readonly AVFrame* _pFrame;
        private readonly AVPacket* _pPacket;

        public StreamDecoder(string url)
        {
            _pFormatContext = ffmpeg.avformat_alloc_context();
            var pFormatContext = _pFormatContext;

            ffmpeg.avformat_open_input(&pFormatContext, url, null, null).ThrowExceptionIfError();
            ffmpeg.avformat_find_stream_info(_pFormatContext, null).ThrowExceptionIfError();

            videoStreamIndex = ffmpeg.av_find_best_stream(_pFormatContext, AVMediaType.AVMEDIA_TYPE_VIDEO, -1, -1, null, 0);
            audioStreamIndex = ffmpeg.av_find_best_stream(_pFormatContext, AVMediaType.AVMEDIA_TYPE_AUDIO, -1, videoStreamIndex, null, 0);

            vcodecContext = _pFormatContext->streams[videoStreamIndex]->codec;
            acodecContext = _pFormatContext->streams[audioStreamIndex]->codec;

            if (videoStreamIndex >= 0)
            {
                AVCodecContext* avctx = OpenStream(vcodecContext);
                FrameSize = new System.Windows.Size(avctx->width, avctx->height);
                PixelFormat = avctx->pix_fmt;
            }

            if (audioStreamIndex >= 0)
                OpenStream(acodecContext);

            _pPacket = ffmpeg.av_packet_alloc();
            _pFrame = ffmpeg.av_frame_alloc();
        }

        public string CodecName { get; }
        public System.Windows.Size FrameSize { get; }
        public AVPixelFormat PixelFormat { get; }
        public int videoStreamIndex { get; }
        public int audioStreamIndex { get; }
        public AVCodecContext* vcodecContext { get; }
        public AVCodecContext* acodecContext { get; }

        private AVCodecContext* OpenStream(AVCodecContext* avctx)
        {
            AVCodec* codec = ffmpeg.avcodec_find_decoder(avctx->codec_id);
            if (codec == null) throw new InvalidOperationException("No codec could be found.");

            avctx->codec_id = codec->id;
            avctx->lowres = 0;
            if (avctx->lowres > codec->max_lowres)
                avctx->lowres = codec->max_lowres;

            avctx->idct_algo = ffmpeg.FF_IDCT_AUTO;
            avctx->error_concealment = 3;

            ffmpeg.avcodec_open2(avctx, codec, null).ThrowExceptionIfError();

            return avctx;
        }

        public void Dispose()
        {
            ffmpeg.av_frame_unref(_pFrame);
            ffmpeg.av_free(_pFrame);

            ffmpeg.av_packet_unref(_pPacket);
            ffmpeg.av_free(_pPacket);

            ffmpeg.avcodec_close(vcodecContext);
            ffmpeg.avcodec_close(acodecContext);

            var pFormatContext = _pFormatContext;
            ffmpeg.avformat_close_input(&pFormatContext);
        }

        public bool TryDecodeNextFrame(out AVFrame frame)
        {
            ffmpeg.av_frame_unref(_pFrame);
            int error;
            do
            {
                try
                {
                    do
                    {
                        error = ffmpeg.av_read_frame(_pFormatContext, _pPacket);
                        if (error == ffmpeg.AVERROR_EOF)
                        {
                            frame = *_pFrame;
                            return false;
                        }

                        error.ThrowExceptionIfError();
                    } while (_pPacket->stream_index != videoStreamIndex);

                    ffmpeg.avcodec_send_packet(vcodecContext, _pPacket).ThrowExceptionIfError();
                }
                finally
                {
                    ffmpeg.av_packet_unref(_pPacket);
                }

                error = ffmpeg.avcodec_receive_frame(vcodecContext, _pFrame);
            } while (error == ffmpeg.AVERROR(ffmpeg.EAGAIN));

            error.ThrowExceptionIfError();
            frame = *_pFrame;
            return true;
        }

        public IReadOnlyDictionary<string, string> GetContextInfo()
        {
            AVDictionaryEntry* tag = null;
            var result = new Dictionary<string, string>();
            while ((tag = ffmpeg.av_dict_get(_pFormatContext->metadata, "", tag, ffmpeg.AV_DICT_IGNORE_SUFFIX)) != null)
            {
                var key = Marshal.PtrToStringAnsi((IntPtr)tag->key);
                var value = Marshal.PtrToStringAnsi((IntPtr)tag->value);
                result.Add(key, value);
            }

            return result;
        }
    }
}

        VideoFrameConverter.cs

using System;
using System.Runtime.InteropServices;
using System.Windows;
using FFmpeg.AutoGen;

namespace MyMediaPlayer.FFmpeg
{
    public sealed unsafe class VideoFrameConverter : IDisposable
    {
        private readonly IntPtr _convertedFrameBufferPtr;
        private readonly Size _destinationSize;
        private readonly byte_ptrArray4 _dstData;
        private readonly int_array4 _dstLinesize;
        private readonly SwsContext* _pConvertContext;

        public VideoFrameConverter(Size sourceSize, AVPixelFormat sourcePixelFormat,
            Size destinationSize, AVPixelFormat destinationPixelFormat)
        {
            _destinationSize = destinationSize;

            _pConvertContext = ffmpeg.sws_getContext(
                (int)sourceSize.Width, 
                (int)sourceSize.Height, 
                sourcePixelFormat, 
                (int)destinationSize.Width, 
                (int)destinationSize.Height, 
                destinationPixelFormat, 
                ffmpeg.SWS_FAST_BILINEAR, null, null, null);

            if (_pConvertContext == null)
                throw new ApplicationException("Could not initialize the conversion context.");

            var convertedFrameBufferSize = ffmpeg.av_image_get_buffer_size(destinationPixelFormat, 
                (int)destinationSize.Width, (int)destinationSize.Height, 1);
            _convertedFrameBufferPtr = Marshal.AllocHGlobal(convertedFrameBufferSize);
            _dstData = new byte_ptrArray4();
            _dstLinesize = new int_array4();

            ffmpeg.av_image_fill_arrays(
                ref _dstData, 
                ref _dstLinesize, 
                (byte*)_convertedFrameBufferPtr, 
                destinationPixelFormat, 
                (int)destinationSize.Width, 
                (int)destinationSize.Height, 1);
        }

        public void Dispose()
        {
            Marshal.FreeHGlobal(_convertedFrameBufferPtr);
            ffmpeg.sws_freeContext(_pConvertContext);
        }

        public AVFrame Convert(AVFrame sourceFrame)
        {
            ffmpeg.sws_scale(_pConvertContext, 
                sourceFrame.data, sourceFrame.linesize, 0, sourceFrame.height, _dstData, _dstLinesize);

            var data = new byte_ptrArray8();
            data.UpdateFrom(_dstData);
            var linesize = new int_array8();
            linesize.UpdateFrom(_dstLinesize);

            return new AVFrame
            {
                data = data,
                linesize = linesize,
                width = (int)_destinationSize.Width,
                height = (int)_destinationSize.Height
            };
        }
    }
}

 

4. 실행 결과

  > 느리지만 디코딩 & 픽셀 이미지 화면이 출력됨.

 

음... 그럼 이번에는 Audio출력과 더불어 pts sync등 정상 재생을 시도해볼까 하는데...

C# 잘 모르는데 공부하면서 하자니 내가 지금 뭐하고 있는거지? 재미삼아 해보는건데...

 

그래서 찾아보니....


[WPF (Windows Presentation Foundation] 2부

 

1. 프로젝트 생성

   // 이름만 다르게. 위와 동일.

 

2. FFmpeg 링크 환경 구성

   > NuGet 패키지 관리 : FFME.Windows 설치

 

3. 연동 코드 작성

 

MainWindows.xaml : ffme namespace & MediaElement 추가.

<Window x:Class="FFME.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:ffme="clr-namespace:Unosquare.FFME;assembly=ffme.win"
        xmlns:local="clr-namespace:FFME"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
        <ffme:MediaElement x:Name="Media" Background="Gray"
            LoadedBehavior="Play" UnloadedBehavior="Manual" />
    </Grid>
</Window>

MainWindow.xaml.cs : FFmpeg 라이브러리 경로 & 파일 정보 추가

using System;
using System.Windows;

namespace FFME
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            Unosquare.FFME.Library.FFmpegDirectory = @"C:\msys64\home\xxx\INSTALLED\bin";
            Media.Source = new Uri(@"D:\Movies\xxxx.mkv");
        }
    }
}

 

 

4. 실행 결과

  > 소리도 잘 나오고, 영상 싱크도 잘 맞는다. 

 

플레이어 제작하는 것도 아니니 여기까지만 하자.

코딩 내용도 별거 없어 github에 올리거나 별도 첨부하지 않았다.

(플레이어 구현 내용이 궁금하면 ffmediaelement참고)

 

 

 

[참고]

 

 

https://github.com/Ruslan-B/FFmpeg.AutoGen/tree/master/FFmpeg.AutoGen.Example

https://github.com/unosquare/ffmediaelement

 

https://stackoverflow.com/questions/44833830/casting-bitmapimage-to-image-source

 

https://docs.microsoft.com/ko-kr/visualstudio/designers/getting-started-with-wpf?view=vs-2019

https://docs.microsoft.com/ko-kr/dotnet/api/system.windows.window.closing?view=netframework-4.8

https://docs.microsoft.com/ko-kr/dotnet/framework/wpf/getting-started/walkthrough-my-first-wpf-desktop-application

 

 

Comments