基於 FFmpeg 的 Cocos Creator 視頻播放器

2021-12-26 騰訊在線教育技術

只為解決一個核心問題,追求更好體驗。

1. 背景

2. 解決方案

3. 任務細分

4. 任務詳情

4.1 移動端 ffplay 播放音視頻

4.2 JSB 綁定視頻組件接口

4.3 視頻展示,紋理渲染

4.4 音頻播放

4.5 優化與擴展

5. 成果展示

6. 參考文檔

1. 背景

騰訊開心鼠項目使用的遊戲引擎是 Cocos Creator,由於引擎提供的視頻組件實現方式問題導致視頻組件和遊戲界面分了層,從而導致了以下若干問題:

核心問題就是分層問題,對於開心鼠項目帶來的最大弊端就是:一套設計,Android,iOS,Web 三端需要各自實現,開發和維護成本高,又因為平臺差異化,還存在視覺不一致和表現不一致問題。

2. 解決方案

因為開心鼠項目需要兼容 Android,iOS 和 Web 三端,Android 和 iOS 一起視為移動端,所以解決方案有以下兩點:

移動端可使用 FFmpeg 庫解碼視頻流,然後使用 OpenGL 來渲染視頻,和使用 Andorid, iOS 兩端各自的音頻接口來播放音頻;網頁端可以直接使用 video 元素來解碼音視頻,然後使用 WebGL 來渲染視頻,和使用 video 元素來播放音頻。3. 任務細分4. 任務詳情4.1 移動端 ffplay 播放音視頻

FFmpeg 官方源碼,可以編譯出三個可執行程序,分別是 ffmpeg, ffplay, ffprobe ,三者作用分別是:

ffmpeg 用於音視頻視頻格式轉換,視頻裁剪等;

其中 ffplay 程序滿足了播放音視頻的需求,理論上,只要把 SDL 視頻展示和音頻播放接口替換成移動端接口,就能完成 Cocos Creator 的音視頻播放功能,但在實際 ffplay 改造過程中,還是會遇到很多小問題,例如:在移動端使用 swscale 進行紋理縮放和像素格式轉換效率低下,不支持 Android asset 文件讀取問題等等,下文會逐一解決。經過一系列改造後,Cocos Creator 可用的 AVPlayer 誕生了。以下為 AVPlayer 播放音視頻流程分析:

概括:

調用 stream_open 函數,初始化狀態信息和數據隊列,並創建 read_thread 和 refresh_thread;read_thread 主要職責為打開流媒體,創建解碼線程(音頻,視頻,字幕),讀取原始數據;解碼線程分別解碼原始數據,得到視頻圖片序列,音頻樣本序列,字幕字符串序列;在創建音頻解碼器過程中,同時打開了音頻設備,在播放過程中,會不斷消耗生成的音頻樣本;refresh_thread 主要職責為不斷消耗視頻圖片序列和字幕字符串序列。

ffplay 改造後的 AVPlayer UML如下:

聲明:因為本人少接觸 c 和 c++ ,所以在 ffplay 改造過程中,SDL 線程改造和字幕分析參考了 bilibili 的 ijkplayer 源碼。

4.2 JSB 綁定視頻組件接口

此節不適合 Web 端,關於 JSB 相關知識,可查閱文檔:JSB 2.0 綁定教程

概括 JSB 功能:通過 ScriptEngine 暴露的接口綁定 JS 對象和其他語言對象,讓 JS 對象控制其他語言對象。

因為播放器邏輯使用 C 和 C++ 編碼,所以需要綁定 JS 和 C++ 對象。上文中的 AVPlayer 只負責解碼和播放流程,播放器還需要處理入參處理,視頻渲染和音頻播放等工作,因此封裝了一個類:Video,其 UML 如下:

Video.cpp 綁定的 JS 對象聲明如下:

bool js_register_video_Video(se::Object *obj) {
    auto cls = se::Class::create("Video", obj, nullptr, _SE(js_gfx_Video_constructor));
    cls->defineFunction("init", _SE(js_gfx_Video_init));
    cls->defineFunction("prepare", _SE(js_gfx_Video_prepare));
    cls->defineFunction("play", _SE(js_gfx_Video_play));
    cls->defineFunction("resume", _SE(js_gfx_Video_resume));
    cls->defineFunction("pause", _SE(js_gfx_Video_pause));
    cls->defineFunction("currentTime", _SE(js_gfx_Video_currentTime));
    cls->defineFunction("addEventListener", _SE(js_gfx_Video_addEventListener));
    cls->defineFunction("stop", _SE(js_gfx_Video_stop));
    cls->defineFunction("clear", _SE(js_gfx_Video_clear));
    cls->defineFunction("setURL", _SE(js_gfx_Video_setURL));
    cls->defineFunction("duration", _SE(js_gfx_Video_duration));
    cls->defineFunction("seek", _SE(js_gfx_Video_seek));
    cls->defineFunction("destroy", _SE(js_cocos2d_Video_destroy));
    cls->defineFinalizeFunction(_SE(js_cocos2d_Video_finalize));
    cls->install();
    JSBClassType::registerClass<cocos2d::renderer::Video>(cls);

    __jsb_cocos2d_renderer_Video_proto = cls->getProto();
    __jsb_cocos2d_renderer_Video_class = cls;

    se::ScriptEngine::getInstance()->clearException();
    return true;
}

bool register_all_video_experiment(se::Object *obj) {
    se::Value nsVal;
    if (!obj->getProperty("gfx", &nsVal)) {
        se::HandleObject jsobj(se::Object::createPlainObject());
        nsVal.setObject(jsobj);
        obj->setProperty("gfx", nsVal);
    }
    se::Object *ns = nsVal.toObject();

    js_register_video_Video(ns);
    return true;
}

概括:以上聲明,表示可在 JS 代碼中,使用以下方法

let video = new gfx.Video();                                // 構造函數

video.init(cc.renderer.device, {                            // 初始化參數
    images: [],                                                         
    width: videoWidth,                                                 
    height: videoHeight,                                                
    wrapS: gfx.WRAP_CLAMP,                                              
    wrapT: gfx.WRAP_CLAMP,
});

video.setURL(url);                                          // 設置資源路徑
video.prepare();                                            // 調用準備函數
video.play();                                               // 播放
video.pause();                                              // 暫停
video.resume();                                             // 恢復
video.stop();                                               // 停止
video.clear();                                              // 清理
video.destroy();                                            // 銷毀
video.seek(position);                                       // 跳轉

let duration = video.duration();                            // 獲取視頻時長
let currentTime = video.currentTime();                      // 獲取當前播放位置

video.addEventListener('loaded', () => {});                 // 監聽 Meta 加載完成事件
video.addEventListener('ready', () => {});                  // 監聽準備完畢事件
video.addEventListener('completed', () => {});              // 監聽播放完成事件
video.addEventListener('error', () => {});                  // 監聽播放失敗事件

4.3 視頻展示,紋理渲染

實現視頻展示功能,需要先了解紋理渲染流程,由於 Cocos Creator 在移動端使用的是 OpenGL API,在 Web 端使用的 WebGL API,OpenGL API 和 WebGL API 大致相同,因此可以到 OpenGL 網站學習下紋理渲染流程。初學者,推薦到 LearnOpenGL CN 學習。接下來使用 LearnOpenGL CN 紋理章節講解以下紋理渲染流程。

頂點著色器:

#version 330 core
layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoord;

out vec2 TexCoord;

void main()
{
gl_Position = vec4(aPos, 1.0);
TexCoord = vec2(aTexCoord.x, aTexCoord.y);
}

片段著色器:

#version 330 core
out vec4 FragColor;

in vec2 TexCoord;

uniform sampler2D tex;

void main()
{
FragColor = texture(tex, TexCoord);
}

紋理渲染程序:

#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <stb_image.h>

#include <learnopengl/shader_s.h>

#include <iostream>

// 窗口大小
const unsigned int SCR_WIDTH = 800;
const unsigned int SCR_HEIGHT = 600;

int main()
{
    // 初始化窗口
    // ---
    GLFWwindow* window = glfwCreateWindow(SCR_WIDTH, SCR_HEIGHT, "LearnOpenGL", NULL, NULL);
    ...

    // 編譯和連結著色器程序
    // -
    Shader ourShader("4.1.texture.vs", "4.1.texture.fs"); 

    // 設置頂點屬性參數
    // ---
    float vertices[] = {
        // 位置                // 紋理坐標
         0.5f,  0.5f, 0.0f,   1.0f, 0.0f, // top right
         0.5f, -0.5f, 0.0f,   1.0f, 1.0f, // bottom right
        -0.5f, -0.5f, 0.0f,   0.0f, 1.0f, // bottom left
        -0.5f,  0.5f, 0.0f,   0.0f, 0.0f  // top left 
    };

    // 設置索引數據,此程序畫的圖形基元是三角形,圖片為矩形,所以由兩個三角形組成
    unsigned int indices[] = {  
        0, 1, 3, // first triangle
        1, 2, 3  // second triangle
    };

    // 聲明和創建 VBO 頂點緩衝對象,VAO 頂點數組對象,索引緩衝對象
    // C 語言並非面向對象編程,這裡使用無符號整形來代表對象
    unsigned int VBO, VAO, EBO;
    glGenVertexArrays(1, &VAO);
    glGenBuffers(1, &VBO);
    glGenBuffers(1, &EBO);

    // 綁定頂點對象數組,可記錄接下來設置的緩衝對象數據,方便在渲染循環中使用
    glBindVertexArray(VAO);

    // 綁定頂點緩衝對象,用於傳遞頂點屬性參數
    glBindBuffer(GL_ARRAY_BUFFER, VBO);
    glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

    // 綁定索引緩衝對象,glDrawElements 會按照索引順序畫圖形基元
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, EBO);
    glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);

    // 連結頂點屬性:位置, 參數:索引,大小,類型,標準化,步進,偏移
    glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)0);
    glEnableVertexAttribArray(0);

    // 連結頂點屬性:紋理坐標,參數:索引,大小,類型,標準化,步進,偏移
    glVertexAttribPointer(1, 2, GL_FLOAT, GL_FALSE, 5 * sizeof(float), (void*)(3 * sizeof(float)));
    glEnableVertexAttribArray(1);


    // 生成紋理對象
    // 
    unsigned int texture;
    glGenTextures(1, &texture);
    glBindTexture(GL_TEXTURE_2D, texture);

    // 設置環繞參數
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
    
    // 設置紋理過濾
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
    glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);

    // 加載圖片
    int width, height, nrChannels;
    unsigned char *data = stbi_load(FileSystem::getPath("resources/textures/container.jpg").c_str(), &width, &height, &nrChannels, 0);
    if (data)
    {
        // 傳遞紋理數據,參數:目標,級別,內部格式,寬,高,邊框,格式,數據類型,像素數組
        glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
        glGenerateMipmap(GL_TEXTURE_2D);
    }
    else
    {
        std::cout << "Failed to load texture" << std::endl;
    }
    stbi_image_free(data);


    // 渲染循環
    // --
    while (!glfwWindowShouldClose(window))
    {
        // 清理
        // ---
        glClearColor(0.2f, 0.3f, 0.3f, 1.0f);
        glClear(GL_COLOR_BUFFER_BIT);

        // 綁定紋理
        glBindTexture(GL_TEXTURE_2D, texture);

        // 應用 shader 程序
        ourShader.use();

        // 綁定頂點數組對象
        glBindVertexArray(VAO);

        // 繪製三角形基元
        glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);

        // glfw 交換緩衝
        glfwSwapBuffers(window);
    }

    // 清理對象
    // --
    glDeleteVertexArrays(1, &VAO);
    glDeleteBuffers(1, &VBO);
    glDeleteBuffers(1, &EBO);

    // 結束
    // -
    glfwTerminate();
    return 0;
}

簡單的紋理渲染流程:

設置頂點數據,包括位置和紋理坐標屬性(值得注意的是:位置坐標系和紋理坐標系不同,下文介紹);創建頂點緩衝對象,索引緩衝對象,頂點數組對象,並綁定傳值;讓程序進入渲染循環,在循環中綁定頂點數組對象,不斷繪製圖形基元。

第2點描述的位置坐標系和紋理系不同,具體不同如下圖:

位置坐標系原點(0,0)在中心位置,x,y 取值範圍是 -1 到 1;紋理坐標系原點(0,0)在左上角位置,x,y取值範圍是 0 到 1;

在 Cocos Creator 2.0 版本後,自定義渲染組件,分為三步:

自定義 Assembler (Assembler 負責傳遞頂點屬性);設置材質動態參數,如設置紋理,變換平移旋轉縮放矩陣等。

第 1 步:著色器程序需要寫在 effect 文件中,而 effect 被 material 使用,每個渲染組件,需要掛載 material 屬性。由於視頻展示,可以理解為圖片幀動畫渲染,因此可以直接使用 Cocos Creator 提供的 CCSprite 所用的 builtin-2d-sprite 材質。

第 2 步:有了材質後,只需要關心位置坐標和紋理坐標傳遞,即要自定義 Assembler,可參考官方文檔 自定義 Assembler。為了效率,直接使用官方源碼 https://github.com/cocos-creator/engine/blob/master/cocos2d/core/renderer/assembler-2d.js 改造,值得注意的是,原生端和 Web 端世界坐標計算方式( updateWorldVerts )不一樣,否則會出現展示位置錯亂問題。直接貼代碼:

export default class CCVideoAssembler extends cc.Assembler {
    constructor () {
        super();
        this.floatsPerVert = 5;
        this.verticesCount = 4;
        this.indicesCount = 6;
        this.uvOffset = 2;
        this.colorOffset = 4;
        this.uv = [0, 1, 1, 1, 0, 0, 1, 0]; // left bottom, right bottom, left top, right top
        this._renderData = new cc.RenderData();
        this._renderData.init(this);
        
        this.initData();
        this.initLocal();
    }

    get verticesFloats () {
        return this.verticesCount * this.floatsPerVert;
    }

    initData () {
        this._renderData.createQuadData(0, this.verticesFloats, this.indicesCount);
    }
    
    initLocal () {
        this._local = [];
        this._local.length = 4;
    }

    updateColor (comp, color) {
        let uintVerts = this._renderData.uintVDatas[0];
        if (!uintVerts) return;
        color = color || comp.node.color._val;
        let floatsPerVert = this.floatsPerVert;
        let colorOffset = this.colorOffset;
        for (let i = colorOffset, l = uintVerts.length; i < l; i += floatsPerVert) {
            uintVerts[i] = color;
        }
    }

    getBuffer () {
        return cc.renderer._handle._meshBuffer;
    }

    updateWorldVerts (comp) {
        let local = this._local;
        let verts = this._renderData.vDatas[0];

        if(CC_JSB){
            let vl = local[0],
            vr = local[2],
            vb = local[1],
            vt = local[3];
            // left bottom
            verts[0] = vl;
            verts[1] = vb;
            // right bottom
            verts[5] = vr;
            verts[6] = vb;
            // left top
            verts[10] = vl;
            verts[11] = vt;
            // right top
            verts[15] = vr;
            verts[16] = vt;
        }else{
            let matrix = comp.node._worldMatrix;
            let matrixm = matrix.m,
                a = matrixm[0], b = matrixm[1], c = matrixm[4], d = matrixm[5],
                tx = matrixm[12], ty = matrixm[13];

            let vl = local[0], vr = local[2],
                vb = local[1], vt = local[3];
            
            let justTranslate = a === 1 && b === 0 && c === 0 && d === 1;

            if (justTranslate) {
                // left bottom
                verts[0] = vl + tx;
                verts[1] = vb + ty;
                // right bottom
                verts[5] = vr + tx;
                verts[6] = vb + ty;
                // left top
                verts[10] = vl + tx;
                verts[11] = vt + ty;
                // right top
                verts[15] = vr + tx;
                verts[16] = vt + ty;
            } else {
                let al = a * vl, ar = a * vr,
                bl = b * vl, br = b * vr,
                cb = c * vb, ct = c * vt,
                db = d * vb, dt = d * vt;

                // left bottom
                verts[0] = al + cb + tx;
                verts[1] = bl + db + ty;
                // right bottom
                verts[5] = ar + cb + tx;
                verts[6] = br + db + ty;
                // left top
                verts[10] = al + ct + tx;
                verts[11] = bl + dt + ty;
                // right top
                verts[15] = ar + ct + tx;
                verts[16] = br + dt + ty;
            }
        }
    }

    fillBuffers (comp, renderer) {
        if (renderer.worldMatDirty) {
            this.updateWorldVerts(comp);
        }

        let renderData = this._renderData;
        let vData = renderData.vDatas[0];
        let iData = renderData.iDatas[0];

        let buffer = this.getBuffer(renderer);
        let offsetInfo = buffer.request(this.verticesCount, this.indicesCount);

        // fill vertices
        let vertexOffset = offsetInfo.byteOffset >> 2,
            vbuf = buffer._vData;

        if (vData.length + vertexOffset > vbuf.length) {
            vbuf.set(vData.subarray(0, vbuf.length - vertexOffset), vertexOffset);
        } else {
            vbuf.set(vData, vertexOffset);
        }

        // fill indices
        let ibuf = buffer._iData,
            indiceOffset = offsetInfo.indiceOffset,
            vertexId = offsetInfo.vertexOffset;
        for (let i = 0, l = iData.length; i < l; i++) {
            ibuf[indiceOffset++] = vertexId + iData[i];
        }
    }

    updateRenderData (comp) {
        if (comp._vertsDirty) {
            this.updateUVs(comp);
            this.updateVerts(comp);
            comp._vertsDirty = false;
        }
    }

    updateUVs (comp) {
        let uv = this.uv;
        let uvOffset = this.uvOffset;
        let floatsPerVert = this.floatsPerVert;
        let verts = this._renderData.vDatas[0];
        for (let i = 0; i < 4; i++) {
            let srcOffset = i * 2;
            let dstOffset = floatsPerVert * i + uvOffset;
            verts[dstOffset] = uv[srcOffset];
            verts[dstOffset + 1] = uv[srcOffset + 1];
        }
    }

    updateVerts (comp) {
        let node = comp.node,
            cw = node.width, ch = node.height,
            appx = node.anchorX * cw, appy = node.anchorY * ch,
            l, b, r, t;
        l = -appx;
        b = -appy;
        r = cw - appx;
        t = ch - appy;

        let local = this._local;
        local[0] = l;
        local[1] = b;
        local[2] = r;
        local[3] = t;
        this.updateWorldVerts(comp);
    }
}

第 3 步,設置材質動態參數,在視頻播放器中,需要動態修改的就是紋理數據了,在移動端,ffplay 改造後的 AVPlayer 在播放過程,通過 ITextureRenderer.render(uint8_t) 接口調用到 void Video::setImage(const uint8_t *data) 方法,實際在不斷更新紋理數據,代碼如下:

void Video::setImage(const uint8_t *data) {
    GL_CHECK(glActiveTexture(GL_TEXTURE0));
    GL_CHECK(glBindTexture(GL_TEXTURE_2D, _glID));
    GL_CHECK(
            glTexImage2D(GL_TEXTURE_2D, 0, _glInternalFormat, _width, _height, 0, _glFormat,
                         _glType, data));
    _device->restoreTexture(0);
}
在 Web 端,則是在 CCVideo 渲染組件的每一幀去傳遞 video 元素,代碼如下:
let gl = cc.renderer.device._gl;
this.update = dt => {
    if(this._currentState == VideoState.PLAYING){
        gl.bindTexture(gl.TEXTURE_2D, this.texture._glID);
        gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, this.video);
    }
};

至此,視頻展示小節完畢。

4.4 音頻播放

在改造音頻播放過程之前,查閱了 ijkplayer 的音頻播放方案,作為現狀分析。

ijkplayer 在 Android 端有兩套方案:AudioTrack 和 OpenSL ES。

AudioTrack 屬於那種同步寫數據的方式,屬於 「推」 方案,google 最開始推行的方式,估計比較穩定,由於 AudioTrack 是 Java 接口,C++ 調用需要反射,理論上,對效率有點影響。OpenSL ES 可以做到 「拉」 方案,但 google 在官方文檔說過對 OpenSL ES 接口沒做太多的兼容,也許不太可靠。

ijkplayer 在 iOS 端也是兩套方案:AudioUint 和 AudioQueue,由於本人對 iOS 開發不熟,不知道二者區別,因此不做展開。

在 Cocos Creator 音頻播放改造中,在 Android 端選擇了 google 最新推行的響應延遲極低的 Google Oboe 方案,Oboe 是 AAudio 和 OpenSL ES 封裝集合,功能更強大,接口更人性化。在 iOS 端選擇了 AudioQueue ,要問原因的話,就是 iOS AudioQueue 的接口和 Android Oboe 提供的接口更像...

音頻播放模型,屬於生產者消費者模型,音頻設備在開啟狀態下,會不斷拉取音頻解碼器生成的音頻樣本。

音頻播放的接口並不複雜,主要用於替換 ffplay 程序中的 SDL 音頻相關接口,具體接口代碼如下:

#ifndef I_AUDIO_DEVICE_H
#define I_AUDIO_DEVICE_H

#include "AudioSpec.h"

class IAudioDevice {

public:

    virtual ~IAudioDevice() {};

    virtual bool open(AudioSpec *wantedSpec) = 0;  // 開啟音頻設備,AudioSpec 結構體包含拉取回調

    virtual void close() = 0;                      // 關閉音頻輸出

    virtual void pause() = 0;                      // 暫停音頻輸出

    virtual void resume() = 0;                     // 恢復音頻輸出

    AudioSpec spec;
};

#endif //I_AUDIO_DEVICE_H

4.5 優化與擴展4.5.1 邊下邊播

邊下邊播可以說是音視頻播放器必備的功能,不但可以節省用戶流量,而且可以提高二次打開速度。最常見的邊下邊播實現方式是在客戶端建立代理伺服器,只需要對播放器傳入的資源路徑加以修改,從而達到播放功能和下載功能解耦。不過理論上,建立代理伺服器會增加行動裝置的內存和電量消耗。

接下來介紹另外一種更簡單易用的方案:利用 FFmpeg 提供的協議組合來實現邊下邊播

在查閱 FFmpeg 官方協議 文檔時,發現某些協議支持組合使用,如下:

cache:http://host/resource

這裡在 http 協議前面添加了 cache 協議,即可以使用官方提供的播放過程中緩存觀看過的一段,以便跳轉使用,由於 cache 協議生成的文件路徑問題,導致移動端不適用,此功能也達不到邊下邊播功能。

但從中可以得到結論:在其他協議前面加入自己協議,就能像鉤子一樣 hook 住其他協議接口,於是整理一個邊下邊播的 avcache 協議:

const URLProtocol av_cache_protocol = {
        .name                = "avcache",
        .url_open2           = av_cache_open,
        .url_read            = av_cache_read,
        .url_seek            = av_cache_seek,
        .url_close           = av_cache_close,
        .priv_data_size      = sizeof(AVContext),
        .priv_data_class     = &av_cache_context_class,
};

原理就是:在 av_cache_read 方法中,調用其他協議的 read 方法,得到數據後,寫入文件並存儲下載信息,並把數據返回給播放器。

4.5.2 libyuv 替換 swscale

YUV(wikipedia),是一種顏色編碼方法。為了節省帶寬,大多數 YUV 格式平均使用的每像素位數都少於24位,因此一般視頻都是用 YUV 顏色編碼。YUV 由分為兩種格式,分別是緊縮格式和平面格式。其中平面格式將 Y、U、V 的三個分量分別存放在不同的矩陣中。

根據上文,如果讓片段著色器直接支持 YUV 紋理渲染,不同格式下,片段著色器所需要的 sampler2D 紋理採樣器數量也不同,因此管理起來相當不便。最簡單的方式,就是把 YUV 顏色編碼轉成 RGB24 顏色編碼,因此需要用到 FFmpeg 提供的 swscale。

但在使用 swscale (已開啟 FFmpeg 編譯選項 neon 優化)進行顏色編碼轉換後,就可以發現 swscale 在移動端效率低下,使用小米 Mix 3 設備,1280x720 解析度的視頻,像素格式從 AV_PIX_FMT_YUV420P 轉成 AV_PIX_FMT_RGB24,縮放按照二次線性採樣,平均耗時高達 16 毫秒,而且導致 CPU 佔用率相當高。數據截圖待補:

經過 google 一番搜索,找到了 google 的 libyuv 替代方案

開源項目:https://chromium.googlesource.com/libyuv/libyuv/

官方優化說明:

Optimized for SSSE3/AVX2 on x86/x64;Optimized for Neon on Arm;Optimized for MSA on Mips。

使用 libyuv 進行像素格式轉換後,使用小米 Mix 3 設備,1280x720 解析度的視頻,像素格式從 AV_PIX_FMT_YUV420P 轉成 AV_PIX_FMT_RGB24,縮放按照二次線性採樣,平均耗時 8 毫秒,相對 swscale 降低了一半。

4.5.3 Android asset 協議

由於 Cocos Creator 本地音視頻資源在 Android 端會打包到 asset 目錄下,在 asset 目錄下的資源需要使用 AssetManager 打開,因此需要支持 Android asset 協議,具體協議聲明如下:

const URLProtocol asset_protocol = {
        .name                = "asset",
        .url_open2           = asset_open,
        .url_read            = asset_read,
        .url_seek            = asset_seek,
        .url_close           = asset_close,
        .priv_data_size      = sizeof(AssetContext),
        .priv_data_class     = &asset_context_class,
};

5. 成果展示6. 參考文檔FFmpeg: https://ffmpeg.org/Cocos Creator 自定義 Assembler: https://docs.cocos.com/creator/manual/zh/advanced-topics/custom-render.html#Cocos Creator JSB 綁定:https://docs.cocos.com/creator/manual/zh/advanced-topics/JSB2.0-learning.htmlAndroid Oboe: https://github.com/google/oboe/blob/master/docs/FullGuide.mdGoogle libyuv: https://chromium.googlesource.com/libyuv/libyuv/+/HEAD/docs/getting_started.mdLearnOpenGL CN: https://learnopengl-cn.github.io/WebGL: https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API/Tutorial/Animating_textures_in_WebGLijkplayer: https://github.com/bilibili/ijkplayer

相關焦點

  • Cocos Creator 3D 物理模塊介紹
    需要注意的是,本文介紹的車輛模擬,是基於現有的物理功能製作的,並不是嚴格的真實車輛模擬,只是一種取巧的方式,而實現此車輛模擬的主要內容是車輛的結構:    校驗,在預覽頁面的控制臺中輸入 CANNON,判斷其是否存在,若存在,則不用執行下列子步驟;(1)將 CANNON 暴露到 Window 中,這裡需要改動引擎代碼:首先點擊編輯器右上角的安裝目錄按鈕,在相對路徑 \resources\3d\engine\cocos\physics\cannon\cannon-world.ts 下,打開該文件
  • ffmpeg音視頻框架的簡介
    ffmpeg是多媒體領域應用最廣泛的一個開源框架,包含了傳輸協議、視頻格式、編解碼、圖像濾鏡、語音處理等功能,並可以與cuda、opencv等這個領域的其他軟體對接,幾乎是事實上的音視頻標準庫。視頻常見的是1280x720,yuv420p。音頻常見的是44100(44.1k)、2聲道、S16,或者48000(48k)、2聲道、S16。他們都有播放時間PTS和解碼時間DTS。
  • CocosCreatorv1.7正式版本發布!
    調試方法請參考JSB 綁定和調試教程https://github.com/cocos-creator/creator-docs/blob/master/zh/advanced-topics/jsb/JSB2.0-learning.md#遠程調試與-profile
  • 精品教程|Cocos Creator資源熱更新
    小夥伴們應該都知道 Cocos Creator 公測版本發布已經1個月了,這段時間裡我們陸續更新了各種文檔,製作人楠大和 panda 獻聲製作了四個教學視頻
  • cocos creator快速集成原生微信登錄分享功能源碼分享
    wechat-quick簡介wechat-quick是基於cocos creator對原生微信登錄和分享功能的一個封裝。項目包含2部分,creator模塊和原生模塊。creator部分是js語言實現,js部分主要功能封裝在一個WechatModule.js文件中,主要包含三部分功能:1.調用oc和java的原生微信登錄分享接口;2.登錄和分享的全局回調函數;
  • cocoscreator 2.4.x版本 drawcall優化 第一期(掌握控制drawcall數量的必要知識)
    8,理論指導實踐,實踐印證理論,demo實操(放在第三期)9,總結(放在第三期)「測試環境」 :1.Mac 系統2.cocoscreator「哪些組件支持渲染:」 因為一個drawcall是一次cpu調用圖形繪製接口命令 gpu進行圖形繪製渲染的過程,所以需要了解cocoscreator中哪些組件支持渲染,才能更好的控制drawcall** * !
  • Cocos Creator 多語言組件實現
    簡介基於cocos creator 2.4.3 的一個手遊項目模板, 提供一些自定義組件以及 Demo。所以之前基於 1.10.3 版本搞了一個使用上更方便的多語言實現, 現在適配到 2.4.3版本, 並添加了對 BMFONT 的支持。繼承cc.Label 內置組件實現, 使用上完全兼容cc.Label。老項目方便接入, vscode 全局查找替換即可。
  • FFMPEG常用的一些命令介紹:音頻錄製、視頻錄製、視頻推流等
    ffmpeg是一個非常快速的視頻和音頻轉換器,也可以從實時音頻/視頻源中獲取。它還可以在任意採樣率之間轉換,並使用高質量的多相濾波器即時調整視頻大小。在實際開發中,ffmpeg除了使用它的API接口進行編程,有些簡單功能可以直接使用ffmpeg命令實現,可以節省很多寫代碼的時間。
  • CocosCreator | Android集成穿山甲SDK(ts和java互調注意事項)
    tips:ts與java交互請參考官方文檔:https://docs.cocos.com/creator/2.2/manual
  • 超好用的萬能視頻播放器,我只推薦恆星播放器
    市面上的視頻播放器軟體五花八門,最常見的就是迅雷看看播放器、暴風影音播放器、搜狐影音播放器、優酷播放器、愛奇藝播放器等等,但是就用戶而言,大家想要的基本上都是純淨的播放器,不要加載一堆無用的東西、不要有廣告、不要大量佔用內存、打開很迅速,操作簡單方便。
  • 分享10款最棒的免費HTML5視頻播放器
    現在有很多的漂亮HTML5視頻播放界面,包括控制元素,所以你不需要其它的東西來播放視頻。這裡我們 給大家介紹10款最好的免費HTML5播放器,希望大家能喜歡,支持我們,請給我們留言!一個免費的開源的使用javascript開發的HTML5播放器。解決了瀏覽器兼容性問題並且添加了很多非標準的強大功能。
  • 實現PC視頻播放最強畫質教程( Potplayer播放器+MADVR插件)
    一、前言這年頭一款趁手好用的播放器,功能強大的播放器是必需的,目前幾乎所有視頻播放器其實只有三種:MPC-HC、Mplayer、VLC player,其餘可以理解為各種殼各種定製版各種修改版。(potplayer的前身km是基於MPC),其中MPC-HC已經內置了LAV Filters,不過Potplayer可定製項更多,更有可玩性,因此選用Potplayer進行搭建目前公認最完美的高清方案顯然是Potplayer(主播放器)+LAV Filters(分離器)+madVR(渲染器)+xy-VSFilter(字幕濾鏡),當然這個所謂的最完美方案是嚴重高能耗的一個方案,就我個人而言一直覺得性質比不佳
  • cocos 使用圖集 - CSDN
    對於圖像資源,為什麼要用圖集,cocos官網的解釋:1.合成圖集時會去除每張圖片周圍的空白區域,加上可以在整體上實施各種優化算法,合成圖集後可以大大減少遊戲包體和內存佔用2.多個構建之後,cocos creator會生成對於的圖集。如果是打包web項目,構建後生成的圖集位置在 build/web-mobile/res/raw-assets下。如果是ios項目,在build/jsb-link/res/raw-assets下
  • cocos 自動圖集 - CSDN
    對於圖像資源,為什麼要用圖集,cocos官網的解釋:1.合成圖集時會去除每張圖片周圍的空白區域,加上可以在整體上實施各種優化算法,合成圖集後可以大大減少遊戲包體和內存佔用2.多個構建之後,cocos creator會生成對於的圖集。如果是打包web項目,構建後生成的圖集位置在 build/web-mobile/res/raw-assets下。如果是ios項目,在build/jsb-link/res/raw-assets下
  • ckplayer:超酷網頁視頻播放器
    網站名稱:超酷網頁視頻播放器-ckplayer網站網址:https://www.ckplayer.com網站簡介:為廣大站長提供的一款可以自定義風格,使用方便,操作簡單
  • 視頻播放器 MPlayer 十周年
    十年後的今天,著名開源播放器正無限接近1.0版,它的最新版是1.0rc3。MPlayer最初版作者Árpád Gereöffy發表文章慶祝了10周年。 MPlayer原名"MPlayer - The Movie Player for Linux",不過後來開發者們簡稱其為"MPlayer - The Movie Player",原因是MPlayer可以在所有平臺上運行。
  • Cocos Creator
    最近剛好在《Cocos Creator遊戲開發實戰》中看到物理系統有一個射線檢測,於是,基於這個射線檢測,寫了一個反覆橫跳的瞄準線效果。一起往下看吧!國際慣例,先上最終效果!在講解之前我們需要一些向量的知識,簡單地介紹一些吧!
  • FFmpeg 內置的一個無中生有的音視頻輸入數據
    而視頻呢,上面兩種應該是比較常用的,其實也不太夠用,尤其是想要逐幀確認,或者測試音頻之類的情況的時候,並且這些視頻一直在電腦裡存著也挺佔地方的,現場下載也挺浪費時間的,所以 FFmpeg 提供了一組雖然看上去不那麼美觀,但是應該足夠用調試和測試用的視頻源數據生成的方法。
  • 音視頻解碼器(LAV Filters)
    音視頻解碼器(LAV Filters) 媒體管理 大小: 12.2M
  • 在iPhone X使用什麼播放器看本地視頻好?博應用ios播放器合集
    大多數視頻播放器,攜帶解碼器以還原經過壓縮媒體文件,視頻播放器還要內置一整套轉換頻率以及緩衝的算法。當然大多數的視頻播放器還能支持播放音頻文件。在蘋果手機上蘋果視頻播放器哪個好呢?1、搜狐影音播放器手機版是搜狐官方推出的在線視頻播放器應用App,搜狐影音手機官方版可點播海量在線電影、電視劇,支持主流媒體格式的視頻、音頻文件,實現本地播放和在線點播的網絡電視軟體。