使用封裝的C++外掛在Unity3d中播放視訊
通過前面三篇文章的講解,我們實現了播放視訊最重要的三個功能:
- 視訊和音訊的解碼
- 音訊的倍速變換
- 音訊播放
接下來,我們需要在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); } }在fixUpdate中通過playtime保證間隔一定的時間載入一幀視訊,這樣視訊音訊才能同步。
後記
唉,這寫這篇文章特別鬱悶,因為大部分都是程式碼,並且程式碼涉及的太多,只能大體介紹流程和需要注意的資訊,上一張圖片然後給連結吧
百度網盤連結:
連結:https://pan.baidu.com/s/1JPrGo0erXDixwQn5fKwm7w
提取碼:ewju