1. 程式人生 > 實用技巧 >使用封裝的C++外掛在Unity3d中播放視訊

使用封裝的C++外掛在Unity3d中播放視訊

通過前面三篇文章的講解,我們實現了播放視訊最重要的三個功能:

  1. 視訊和音訊的解碼
  2. 音訊的倍速變換
  3. 音訊播放

接下來,我們需要在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