视频播放器-FFMPEG官方库,包含lib,include,bin x64和x86平台的所有文件,提取码4v2c
视频播放器-LQVideo实现视频解码C++源代码,提取码br9u
视频播放器-SoundTouch实现声音变速的C++源代码,提取码6htk
通过前面三篇文章的讲解,我们实现了播放视频最重要的三个功能:
- 视频和音频的解码
- 音频的倍速变换
- 音频播放
接下来,我们需要在unity3d中使用封装好的C++插件实现视频的播放,我们现在主要是以windows PC为主,后面如果有时间,我会实现安卓和IOS的跨平台
本篇文章我们实现在unity3d中视频和音频的播放,在下一篇文章中,我们会使用unity3d引擎封装一个完整的时候播放器,接下来我们进行第一步操作,把dll动态链接库拷贝到unity3d工程的Plugins文件夹下, 我是放在“Plugins\libs\win\release_64”下边了,只是单纯的为了好区分
然后是第二步操作,将所有的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); } }
后记
唉,这写这篇文章特别郁闷,因为大部分都是代码,并且代码涉及的太多,只能大体介绍流程和需要注意的信息,上一张图片然后给链接吧
百度网盘链接:
链接:https://pan.baidu.com/s/1JPrGo0erXDixwQn5fKwm7w
提取码:ewju