• 视频播放器-使用封装的C++插件在Unity3d中播放视频


    视频播放器-视频播放前期调研

    视频播放器-使用FFMPEG技术对视频解封装和解码

    视频播放器-使用SoundTouch算法库对声音进行变速

    视频播放器-使用OpenAL技术播放声音

    视频播放器-使用封装的C++插件在Unity3d中播放视频

    视频播放器-FFMPEG官方库,包含lib,include,bin x64和x86平台的所有文件,提取码4v2c

    视频播放器-LQVideo实现视频解码C++源代码,提取码br9u

    视频播放器-SoundTouch实现声音变速的C++源代码,提取码6htk

    视频播放器-官方openal安装文件,提取码yl3j

    视频播放器-OpenAL实现音频播放功能,提取码mjp2

     

    通过前面三篇文章的讲解,我们实现了播放视频最重要的三个功能:

    1. 视频和音频的解码
    2. 音频的倍速变换
    3. 音频播放

    接下来,我们需要在unity3d中使用封装好的C++插件实现视频的播放,我们现在主要是以windows PC为主,后面如果有时间,我会实现安卓和IOS的跨平台

    本篇文章我们实现在unity3d中视频和音频的播放,在下一篇文章中,我们会使用unity3d引擎封装一个完整的时候播放器,接下来我们进行第一步操作,把dll动态链接库拷贝到unity3d工程的Plugins文件夹下, 我是放在“Pluginslibswin elease_64”下边了,只是单纯的为了好区分

    image

    然后是第二步操作,将所有的C++接口导入到unity3d中,我们单独新建一个脚本文件LQPlayerDllImport.cs专门放所有的接口

    class LQPlayerDllImport
        {
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int init_ffmpeg(String url);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int read_video_frame(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int read_video_frames(int key, int count);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int read_audio_frame(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern IntPtr get_audio_frame(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern void set_audio_disabled(int key, bool disabled);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern IntPtr get_video_frame(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int get_audio_buffer_size(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int get_video_buffer_size(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int get_video_width(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int get_video_height(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int get_video_length(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern double get_video_frameRate(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int get_audio_sample_rate(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int get_audio_channel(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern bool seek_video(int key, int time);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern double get_current_time(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern double get_audio_time(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern void release(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int get_version();
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int read_frame_packet(int key);
            [DllImport("LQVideo", CallingConvention = CallingConvention.Cdecl)]
            public static extern int get_first_video_frame(int key);
    
            [DllImport("OpenALSound", CallingConvention = CallingConvention.Cdecl)]
            public static extern int InitOpenAL();
            [DllImport("OpenALSound", CallingConvention = CallingConvention.Cdecl)]
            public static extern int SetSoundPitch(int key, float value);
            [DllImport("OpenALSound", CallingConvention = CallingConvention.Cdecl)]
            public static extern int SetSoundPlay(int key);
            [DllImport("OpenALSound", CallingConvention = CallingConvention.Cdecl)]
            public static extern int SetSoundPause(int key);
            [DllImport("OpenALSound", CallingConvention = CallingConvention.Cdecl)]
            public static extern int SetSoundStop(int key);
            [DllImport("OpenALSound", CallingConvention = CallingConvention.Cdecl)]
            public static extern int SetSoundRewind(int key);
            [DllImport("OpenALSound", CallingConvention = CallingConvention.Cdecl)]
            public static extern bool HasProcessedBuffer(int key);
            [DllImport("OpenALSound", CallingConvention = CallingConvention.Cdecl)]
            public static extern int SendBuffer(int key, byte[] data, int length);
            [DllImport("OpenALSound", CallingConvention = CallingConvention.Cdecl)]
            public static extern int SetSampleRate(int key, short channels, short bit, int samples);
            [DllImport("OpenALSound", CallingConvention = CallingConvention.Cdecl)]
            public static extern int Reset(int key);
            [DllImport("OpenALSound", CallingConvention = CallingConvention.Cdecl)]
            public static extern int Clear(int key);
            [DllImport("OpenALSound", CallingConvention = CallingConvention.Cdecl)]
            public static extern int SetVolumn(int key, float value);
    
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern void CreateInstance(int key);
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern void DestroyInstance(int key);
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern void SetRate(double rate, int key);
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern void SetTempo(double value, int key);
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern void SetPitch(double value, int key);
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern void SetChannel(uint value, int key);
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern void SetSampleRate(uint value, int key);
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern void Flush(int key);
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern void PutSample(float[] data, uint sampleLength, int key);
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern void PutSampleShort(short[] data, uint sampleLength, int key);
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern uint GetSample(float[] data, uint sampleLength, int key);
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern uint GetSampleShort(short[] data, uint sampleLength, int key);
            [DllImport("LQAudio", CallingConvention = CallingConvention.Cdecl)]
            public static extern uint GetSampleNum(int key);
        }

    这里需要注意的一点,OpenAL的接口我们添加了参数key值,主要是为了能同时播放多个声音,因为我们视频能同时播放10个,音频我们也是设置了10个,如果读者不知道怎么处理,在系列文章的最后,我会提交所有接口的完整代码。

    接下来是我们的第二个脚本,这个脚本是视频播放的核心脚本LQVideoPlayer.cs。

    在写代码之前,我们应该先考虑怎么实现视频的播放过程呢,我想视频的播放大概需要以下几个步骤:

    • 初始化视频插件信息,获取帧速,采样率等信息
    • 初始化音频插件信息
    • 异步读取视频的Packet包
    • 添加缓存队列,然后异步读取解码后的视频信息到缓存队列,读取音频数据到音频缓存数据中
    • 根据帧速,定时将视频数据生成纹理进行显示,播放声音数据

    在详细介绍每一步骤的实现之前需要先做几点说明

    • 初始化Packet包这一步按道理应该在C++底层实现,我之前的文章说过,总是出异常,所以我把这一步拿到了unity中,也没有什么问题。
    • 获取Packet包和解码后的视频数据,音频数据这两步应该是异步加载的,为了方便,我们在update中实现,但是我故意在这两步的处理中没有使用任何UI的东西,所以可以直接使用Thread起线程实现,也可以使用协程。我们的项目对性能要求很高,并且视频都是2K的视频。所以我在项目中使用线程实现的。
    • 读取音频数据的时候必须保证比视频帧的时间向后一点,不然可能导致卡顿。
    • 视频数据因为是解码以后的所以是很大的,每一帧差不多10+M,我们的缓存队列不能存储太多,并且我们的队列需要有两个特点,具有队列的先进先出特性,队列必须是环形的以避免不断的分配内存占用内存。

    好了,接下来对每一步进行详细分析

    创建环形队列

    直接上代码了,应该能看懂

    public class Circle<T>
    {
        /// <summary>
        /// 数据数组
        /// </summary>
        public T[] data;
        /// <summary>
        /// 开始索引,每次出队列开始索引+1
        /// </summary>
        public int start =0;
        /// <summary>
        /// 结束索引,每次进队列结束索引+1
        /// </summary>
        public int end =0;
        /// <summary>
        /// 层级,如果结束索引超过了最大值,因为是环形,所以结束索引会从0重新开始,
        /// 为了标记这一特性,grade设置为1,其实简单说就是结束索引不和开始索引在一圈上了
        /// </summary>
        int grade=0;
        /// <summary>
        /// 环形队列的最大值
        /// </summary>
        int max = 0;
        private System.Object lockObj = new System.Object();
    
        public Circle(int count)
        {
            data = new T[count];
            max = count;
        }
        /// <summary>
        /// 向队列添加假定数据
        /// </summary>
        public void PushNone()
        {
            if (Size() >= max)
            {
                return;
            }
            lock (lockObj)
            {
                if (end == max - 1)
                {
                    end = 0;
                    grade = 1;
                }
                else
                {
                    end++;
                }
            }
        }
    
        /// <summary>
        /// 假定从队列拿出数据
        /// </summary>
        public void PopNone()
        {
            if (Size() == 0)
            {
                return;
            }
            lock (lockObj)
            {
                if (start == max - 1)
                {
                    start = 0;
                    grade = 0;
                }
                else
                {
                    start++;
                }
            }
        }
    
        public void Clear()
        {
            start = 0;
            end = 0;
            grade = 0;
        }
    
        public int Size()
        {
            return (grade == 0) ? (end - start) : (end + (max - start));
        }
    }

    里面有两个方法,PushNone()和PopNone(),为什么是假定数据呢,因为数据我们直接调用data字段添加了,所以只是更新开始索引和结束索引而已。

     

    初始化视频插件

    /// <summary>
        /// 初始化视频信息
        /// </summary>
        void InitVideo()
        {
            this.initFfmpeg = LQPlayerDllImport.init_ffmpeg(path);
    
            if (initFfmpeg >= 0)
            {
                this.sampleRate = LQPlayerDllImport.get_audio_sample_rate(initFfmpeg);
                this.channel = LQPlayerDllImport.get_audio_channel(initFfmpeg);
                this.frame_rate = LQPlayerDllImport.get_video_frameRate(initFfmpeg);
                this.videoWidth = LQPlayerDllImport.get_video_width(initFfmpeg);
                this.videoHeight = LQPlayerDllImport.get_video_height(initFfmpeg);
                this.frameInterval = (float)(1.0f / this.frame_rate);
                this.totalTime = LQPlayerDllImport.get_video_length(initFfmpeg);
                LogUtils.GetInstance().WriteLog("视频组件初始化成功,当前视频索引【key】:" + initFfmpeg);
            }
            else
            {
                LogUtils.GetInstance().WriteLog("视频初始化失败,请检查视频路径", LogUtils.LogTypes.ERROR);
            }
        }

    通过将视频的路径作为参数初始化视频插件,我们获取到了是否初始化成功,采样率,声道,帧速,视频宽度,视频高度,视频采样间隔,视频总时长信息

    /// <summary>
        /// 初始化视频信息
        /// </summary>
        void InitVideo()
        {
            this.initFfmpeg = LQPlayerDllImport.init_ffmpeg(path);
    
            if (initFfmpeg >= 0)
            {
                this.sampleRate = LQPlayerDllImport.get_audio_sample_rate(initFfmpeg);
                this.channel = LQPlayerDllImport.get_audio_channel(initFfmpeg);
                this.frame_rate = LQPlayerDllImport.get_video_frameRate(initFfmpeg);
                this.videoWidth = LQPlayerDllImport.get_video_width(initFfmpeg);
                this.videoHeight = LQPlayerDllImport.get_video_height(initFfmpeg);
                this.frameInterval = (float)(1.0f / this.frame_rate);
                this.totalTime = LQPlayerDllImport.get_video_length(initFfmpeg);
                LogUtils.GetInstance().WriteLog("视频组件初始化成功,当前视频索引【key】:" + initFfmpeg);
            }
            else
            {
                LogUtils.GetInstance().WriteLog("视频初始化失败,请检查视频路径", LogUtils.LogTypes.ERROR);
            }
        }
    
        public void Start()
        {
            Init();
            if (initFfmpeg >= 0)
            {
                LogUtils.GetInstance().WriteCurrentTime("Start Video Player:" + initFfmpeg);
                keyList.Add(initFfmpeg);
                this.showImg = this.transform.Find("texture0").gameObject.GetComponent<RawImage>();
                if (this.waitForFirstFrame)
                {
                    this.InitFirstFrame();
                }
                this.InitDataInfo();
                this.audioPlayer = this.GetComponent<LQAudioPlayer>();
                this.InitAudio();
                LogUtils.GetInstance().WriteCurrentTime("End Start Video Player:" + initFfmpeg);
            }
        }

    上面这三部是初始化数据代码,里面有没实现的方法,不要紧,完整代码中会有实现。

    获取Packet数据包和解码后的数据

    void Update()
        {
            LogUtils.GetInstance().WriteCurrentTime("Start Video Update:" + initFfmpeg);
            int ret = 0;
            ret = LQPlayerDllImport.read_frame_packet(initFfmpeg);
            while (frameCircle.Size() < FrameCacheCount - 2 && !this.isVideoEnd)
            {
                frame_type = LQPlayerDllImport.read_video_frame(initFfmpeg);
                LogUtils.GetInstance().WriteCurrentTime("read_video_frame:" + initFfmpeg);
                // 跳转或者一般错误
                if (frame_type == -1 || frame_type == -2)
                {
                    break;
                }
                else if (frame_type == -3)//结束
                {
                    this.isVideoEnd = true;
                    break;
                }
                else if (frame_type == 2)//加载视频帧成功
                {
                    this.AddVideoWithSpeed();
                    this.AddAudioFrame();
                    break;
                }
            }
            LogUtils.GetInstance().WriteCurrentTime("End Video Update:" + initFfmpeg);
        }

    AddVideoWithSpeed()是根据倍速添加视频数据,this.AddAudioFrame()是添加音频数据

    显示视频纹理和播放音频

    private void FixedUpdate()
        {
            if (string.IsNullOrEmpty(path) || this.initFfmpeg < 0)
            {
                return;
            }
            if (playState == VideoPlayState.Playing)
            {
                this.playTime += UnityEngine.Time.fixedDeltaTime;
                if (this.playTime >= frameInterval)
                {
                    this.LoadFrame();
                    this.playTime -= frameInterval;
                }
            }
        }
    
        /// <summary>
        /// 加载视频一帧图像和音频
        /// </summary>
        private void LoadFrame()
        {
            if (frameCircle == null)
            {
                return;
            }
            if (frameCircle.Size() <= 0)
            {
                // 表明还没有预加载足够的缓存数据
                if (!isVideoEnd)
                {
                    return;
                }
                if (this.IsLoop)
                {
                    this.Seek(0);
                }
                else
                {
                    this.playState = VideoPlayState.End;
                }
                return;
            }
            this.transform.localEulerAngles = new Vector3(180, 0, 0);
            frameCircle.data[frameCircle.start].LoadTexture();
            this.showImg.texture = frameCircle.data[frameCircle.start].textureImg;
            this.time = frameCircle.data[frameCircle.start].time;
            this.frameCircle.PopNone();
            if (isSeeking)
            {
                LogUtils.GetInstance().WriteCurrentTime("跳转执行完成:" + initFfmpeg);
                isSeeking = false;
                subTitleIndex = 0;
            }
            if (!audioDisable)
            {
                audioPlayer.SetVolumn(volumn);
            }
        }
    在fixUpdate中通过playtime保证间隔一定的时间加载一帧视频,这样视频音频才能同步。

    后记

    唉,这写这篇文章特别郁闷,因为大部分都是代码,并且代码涉及的太多,只能大体介绍流程和需要注意的信息,上一张图片然后给链接吧

    image

    百度网盘链接

    链接:https://pan.baidu.com/s/1JPrGo0erXDixwQn5fKwm7w
    提取码:ewju

  • 相关阅读:
    接口测试——Java + TestNG 国家气象局接口(json解析)实例
    log4j2配置文件解读
    ReportNG报表显示中文乱码和TestNG显示中文乱码实力解决办法
    Jmeter脚本录制方法——手工编写脚本(jmeter与fiddler结合使用)
    SQL server学习(五)T-SQL编程之存储过程
    SQL server学习(四)T-SQL编程之事务、索引和视图
    Jmeter——关联(正则表达式)
    Jmeter脚本录制方法——Badboy录制&自带的代理服务器录制
    Jmeter——环境搭建
    SpringBoot系列之集成Dubbo示例教程
  • 原文地址:https://www.cnblogs.com/sauronKing/p/13474572.html
Copyright © 2020-2023  润新知