일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Android
- RTMP
- aws cli
- HLS
- Windows10
- Shell script
- Kubernetes
- spring cloud config
- wireshark
- VSCode
- namespace
- configmap
- dart
- Java
- ebpf
- Python
- macos
- ffmpeg
- nginx-media-server
- deployment
- golang
- aws
- docker
- Pod
- android studio
- Sysinternals
- 행정구역분류
- service
- Flutter
- kubectl
- Today
- Total
woonizzooni
FFmpeg Visual Studio 2019 컴파일-2 (동영상 플레이어, C++/MFC, C#/WPF) 본문
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 : 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
'FFmpeg' 카테고리의 다른 글
[Windows] ffmpeg으로 화면캡처(capture desktop)해서 rtmp송출 (0) | 2020.11.08 |
---|---|
[macos] ffmpeg으로 화면캡처(capture desktop)해서 rtmp송출 (0) | 2020.03.29 |
macOS ffmpeg 설치 (0) | 2020.01.27 |
ffplay 분석 (0) | 2019.07.24 |
FFmpeg Visual Studio 2019 컴파일-1 (0) | 2019.07.10 |