FFmpeg 从零开始开发简单的音视频播放器(七)(完结)

c#音视频播放器

一、简单说明

        前面FFmpeg 从零开始开发简单的音视频播放器(五)中,我们已经完成了一个没有声音的纯视频播放器,现在我们要在该视频播放器的基础上进行改造,添加音频播放功能。通过上一节FFmpeg 从零开始开发简单的音视频播放器(六)的开发,我们已经完成了c++部分的音频解码功能,现在就接将它整合到视频播放器中。

二、暴露c++接口

        在头文件中,添加以下函数接口:

//url:文件地址
extern "C" _declspec(dllexport) int init_ffmpeg(char* url);

//读取一帧 -1:未取到 1:音频 2:视频 
extern "C" _declspec(dllexport) int read_frame();

//获取音频帧
extern "C" _declspec(dllexport) char *get_audio_frame();

//获取视频帧
extern "C" _declspec(dllexport) char *get_video_frame();

//获取音频缓存大小
extern "C" _declspec(dllexport) int get_audio_buffer_size();

//获取视频缓存大小
extern "C" _declspec(dllexport) int get_video_buffer_size();

//获取视频宽度
extern "C" _declspec(dllexport) int get_video_width();

//获取视频高度
extern "C" _declspec(dllexport) int get_video_height();

//释放资源
extern "C" _declspec(dllexport) void release();

三、将c++项目改造为动态链接库(dll)项目

1、右击c++项目-->属性-->配置属性-->常规-->项目默认值-->配置类型-->选择动态库,操作如下图所示:

-------------------------------------------------------------------------
          注意:这里采用的是vs默认的Debug配置,平台采用的是vs默认的win32平台。开发的同学可以根据自己的需求,在这个页面进行配置。
        2、重新生成c++项目:
FFmpeg 从零开始开发简单的音视频播放器(七)(完结)_第1张图片
--------------------------------------------------------------------------------
        现在项目的生成的已经不再是exe文件了。如上图,动态链接库已经生成完毕。

四、配置c#项目

        1、将c#项目设置为启动项目:

-----------------------------------------------------------------------------------------

        2、添加NAudio播放组件:右击c#项目-->管理NuGet程序包-->浏览中输入“NAudio”-->下载并安装

FFmpeg 从零开始开发简单的音视频播放器(七)(完结)_第2张图片

-------------------------------------------------------------------------------

FFmpeg 从零开始开发简单的音视频播放器(七)(完结)_第3张图片

------------------------------------------------------------------------------

FFmpeg 从零开始开发简单的音视频播放器(七)(完结)_第4张图片

------------------------------------------------------------------------------

        3、编写wpf后台代码

using NAudio.Wave;
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
using System.Threading;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace FFmpegPlayer
{
    /// 
    /// MainWindow.xaml 的交互逻辑
    /// 
    public partial class MainWindow : Window
    {
        [DllImport("FFmpegDll.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern int init_ffmpeg(IntPtr url);

        [DllImport("FFmpegDll.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern int read_frame();

        [DllImport("FFmpegDll.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern IntPtr get_audio_frame();

        [DllImport("FFmpegDll.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern IntPtr get_video_frame();

        [DllImport("FFmpegDll.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern int get_audio_buffer_size();

        [DllImport("FFmpegDll.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern int get_video_buffer_size();

        [DllImport("FFmpegDll.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern int get_video_width();

        [DllImport("FFmpegDll.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern int get_video_height();

        [DllImport("FFmpegDll.dll", CallingConvention = CallingConvention.Cdecl)]
        public static extern void release();

        //播放状态
        private bool isPlaying = false;

        //缓存时间(ms)
        const int BUFFER_DURATION = 2000;

        //NAudio音频播放组件
        private WaveOut waveOut;
        private BufferedWaveProvider bufferedWaveProvider;

        public MainWindow()
        {
            InitializeComponent();
        }

        private void Window_Loaded(object sender, RoutedEventArgs e)
        {
            init();
            new Thread(play).Start();
        }

        protected override void OnClosing(CancelEventArgs e)
        {
            MessageBoxResult result = MessageBox.Show("退出播放器吗?", "提示", MessageBoxButton.YesNo);
            if (result.Equals(MessageBoxResult.Yes))
            {
                base.OnClosing(e);
                isPlaying = false;
            }
            else
            {
                e.Cancel = true;
            }
        }

        /// 
        /// 做一些初始化操作
        /// 
        private void init()
        {
            waveOut = new WaveOut();
            bufferedWaveProvider = new BufferedWaveProvider(new WaveFormat());
            waveOut.Init(bufferedWaveProvider);
            waveOut.Play();
        }

        /// 
        /// 播放
        /// 
        void play()
        {
            isPlaying = true;

            bool isInitOk = false;
            while (isPlaying && !isInitOk)
            {
                //初始化ffmpeg
                int initFfmpeg = init_ffmpeg(mallocIntPtr("rtmp://live.hkstv.hk.lxdns.com/live/hks"));
                if (initFfmpeg >= 0)
                {
                    isInitOk = true;
                    break;
                }
            }

            TimeSpan audioMaxBufferedDuration = new TimeSpan(0, 0, 0, 0, BUFFER_DURATION); //音频最大缓冲时间

            while (isPlaying)
            {
                int i = read_frame(); //读取一帧 1:音频 2:视频
                if (i == 1)
                {
                    if (bufferedWaveProvider.BufferedDuration.CompareTo(audioMaxBufferedDuration) > 0)
                    {
                        bufferedWaveProvider.ClearBuffer();
                    }

                    IntPtr audioFrame = get_audio_frame(); //一帧音频句柄
                    int bufSize = get_audio_buffer_size(); //一帧音频占用大小
                    byte[] bytes = new byte[bufSize];
                    Marshal.Copy(audioFrame, bytes, 0, bufSize); //拷贝句柄中的音频到bytes
                    bufferedWaveProvider.AddSamples(bytes, 0, bufSize);//向缓存中添加音频样本
                }
                else if (i == 2)
                {
                    Dispatcher.Invoke(new Action(delegate
                    {
                        BitmapSource bs = BitmapImage.Create(
                            get_video_width(),
                            get_video_height(),
                            0,
                            0,
                            PixelFormats.Rgb24,
                            null,
                            get_video_frame(),
                            get_video_buffer_size(),
                            (get_video_width() * PixelFormats.Rgb24.BitsPerPixel + 7) / 8);
                        Thread t = new Thread(new ParameterizedThreadStart(playVideo));
                        t.Start(bs);
                    }));
                }
            }

            try
            {
                release();
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
            }
        }

        /// 
        /// 播放视频,单独开线程,是为了和音频有同步的缓存时间
        /// 
        void playVideo(object bs)
        {
            int time = BUFFER_DURATION - 300;
            if (time > 0)
            {
                Thread.Sleep(time);
            }
            Dispatcher.BeginInvoke(new Action(delegate { videoImg.Source = bs as BitmapSource; }));
        }

        /// 
        /// 将字符串分配到非托管区,并返回该非托管区句柄
        /// 
        /// 字符串
        /// 非托管区句柄
        private IntPtr mallocIntPtr(String strData)
        {
            //字符串转化为字节
            Byte[] bytesData = System.Text.Encoding.Default.GetBytes(strData);

            //分配非托管区内存
            IntPtr ptr = Marshal.AllocHGlobal(bytesData.Length);

            //清空非托管区
            Byte[] emptyBytes = new Byte[bytesData.Length + 1]; //一定要加1,否则后面是乱码,原因未找到
            Marshal.Copy(emptyBytes, 0, ptr, emptyBytes.Length);

            //往非托管区塞数据
            Marshal.Copy(bytesData, 0, ptr, bytesData.Length);

            return ptr;
        }
    }
}

       代码说明:

  • WaveOut:NAudio播放组件,可以用来播放原始PCM格式音频,pcm格式是FFmpeg解码后的默认格式;
  • BufferedWaveProvider:可以存放音频字节缓存,使用的时候,要注意缓存时间和缓存大小;
  • void playVideo(object bs)方法:单开了一个线程用于视频显示,并且在线程里面sleep了一段时间。这里处理音视频同步的简便方法,是采用了sleep时间和音频缓存时间保持一致的方式。当然最科学的方法是加上视频缓存,然后匹配视频和音频的时间轴,音视频时间轴在FFmpeg的c++解码后是可以获取到的,我们写的是简单的播放器,就不扩展了。

         4、拷贝第三步生成的dll文件到执行目录下:

---------------------------------------------------------------------------------

         5、重新生成并运行c#项目,运行效果如下:

FFmpeg 从零开始开发简单的音视频播放器(七)(完结)_第5张图片
---------------------------------------------------------------------------------------------

        好了,带上耳机,感受一下成果吧。

        注意:由于官方编译的新版本FFmpeg已经不支持xp了,如果有的同学希望在xp下运行播放器,需要下载一个“FixFFmpeg”工具,对FFmpeg的函数做一个映射。

        从零开始开发简单的音视频播放器就此完结,第一次接触FFmpeg,有不足的地方,大家多多提点。

你可能感兴趣的:(wpf,c++,FFmpeg,c#)