SauronKing

写随笔只是为了记录自己的曾经,如果能给您带来些许方便,那是我莫大的荣幸!

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

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

视频播放器-使用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文件夹下, 我是放在“Plugins\libs\win\release_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

posted on 2020-08-11 11:36  SauronKing  阅读(898)  评论(1编辑  收藏  举报