• 视频播放器-使用封装的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

  • 相关阅读:
    左孩子右兄弟的字典树
    UVA 1401 Remember the Word
    HDOJ 4770 Lights Against Dudely
    UvaLA 3938 "Ray, Pass me the dishes!"
    UVA
    Codeforces 215A A.Sereja and Coat Rack
    Codeforces 215B B.Sereja and Suffixes
    HDU 4788 Hard Disk Drive
    HDU 2095 find your present (2)
    图的连通性问题—学习笔记
  • 原文地址:https://www.cnblogs.com/sauronKing/p/13474572.html
Copyright © 2020-2023  润新知