1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        騰訊的技術(shù)...真的太牛逼了!

        共 7892字,需瀏覽 16分鐘

         ·

        2021-01-06 11:48

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

        • 1. 背景

        • 2. 解決方案

        • 3. 任務(wù)細分

        • 4. 任務(wù)詳情

          • 4.1 移動端 ffplay 播放音視頻

          • 4.2 JSB 綁定視頻組件接口

          • 4.3 視頻展示,紋理渲染

          • 4.4 音頻播放

          • 4.5 優(yōu)化與擴展

        • 5. 成果展示

        • 6. 參考文檔

        1. 背景

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

        1. 不可以在視頻組件上添加其他渲染組件;
        2. 不可以使用遮罩組件來限定視頻形狀;
        3. 退出場景時存在視頻組件殘影;
        4. 等等...

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

        2. 解決方案

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

        1. 移動端可使用 FFmpeg 庫解碼視頻流,然后使用 OpenGL 來渲染視頻,和使用 Andorid, iOS 兩端各自的音頻接口來播放音頻;
        2. 網(wǎng)頁端可以直接使用 video 元素來解碼音視頻,然后使用 WebGL 來渲染視頻,和使用 video 元素來播放音頻。

        3. 任務(wù)細分

        4. 任務(wù)詳情

        4.1 移動端 ffplay 播放音視頻

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

        1. ffmpeg 用于音視頻視頻格式轉(zhuǎn)換,視頻裁剪等;
        2. ffplay 用于播放音視頻,需要依賴 SDL;
        3. ffprobe 用于分析音視頻流媒體數(shù)據(jù)。

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

        概括:

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

        ffplay 改造后的 AVPlayer UML如下:

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

        4.2 JSB 綁定視頻組件接口

        此節(jié)不適合 Web 端,關(guān)于 JSB 相關(guān)知識,可查閱文檔:JSB 2.0 綁定教程

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

        因為播放器邏輯使用 C 和 C++ 編碼,所以需要綁定 JS 和 C++ 對象。上文中的 AVPlayer 只負責解碼和播放流程,播放器還需要處理入?yún)⑻幚?,視頻渲染和音頻播放等工作,因此封裝了一個類: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(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();????????????????????????????????//?構(gòu)造函數(shù)

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

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

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

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

        4.3 視頻展示,紋理渲染

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

        頂點著色器:

        #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?
        #include?
        #include?

        #include?

        #include?

        //?窗口大小
        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");?

        ????//?設(shè)置頂點屬性參數(shù)
        ????//?-------------
        ????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?
        ????};

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

        ????//?聲明和創(chuàng)建?VBO?頂點緩沖對象,VAO?頂點數(shù)組對象,索引緩沖對象
        ????//?C?語言并非面向?qū)ο缶幊?,這里使用無符號整形來代表對象
        ????unsigned?int?VBO,?VAO,?EBO;
        ????glGenVertexArrays(1,?&VAO);
        ????glGenBuffers(1,?&VBO);
        ????glGenBuffers(1,?&EBO);

        ????//?綁定頂點對象數(shù)組,可記錄接下來設(shè)置的緩沖對象數(shù)據(jù),方便在渲染循環(huán)中使用
        ????glBindVertexArray(VAO);

        ????//?綁定頂點緩沖對象,用于傳遞頂點屬性參數(shù)
        ????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);

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

        ????//?鏈接頂點屬性:紋理坐標,參數(shù):索引,大小,類型,標準化,步進,偏移
        ????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);

        ????//?設(shè)置環(huán)繞參數(shù)
        ????glTexParameteri(GL_TEXTURE_2D,?GL_TEXTURE_WRAP_S,?GL_REPEAT);
        ????glTexParameteri(GL_TEXTURE_2D,?GL_TEXTURE_WRAP_T,?GL_REPEAT);
        ????
        ????//?設(shè)置紋理過濾
        ????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)
        ????{
        ????????//?傳遞紋理數(shù)據(jù),參數(shù):目標,級別,內(nèi)部格式,寬,高,邊框,格式,數(shù)據(jù)類型,像素數(shù)組
        ????????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);


        ????//?渲染循環(huán)
        ????//?-------
        ????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();

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

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

        ????????//?glfw?交換緩沖
        ????????glfwSwapBuffers(window);
        ????}

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

        ????//?結(jié)束
        ????//?------------------------------------------------------------------
        ????glfwTerminate();
        ????return?0;
        }

        簡單的紋理渲染流程:

        1. 編譯和鏈接著色器程序;
        2. 設(shè)置頂點數(shù)據(jù),包括位置和紋理坐標屬性(值得注意的是:位置坐標系和紋理坐標系不同,下文介紹);
        3. 設(shè)置索引數(shù)據(jù),索引是用來繪制圖形基元時參照;
        4. 創(chuàng)建頂點緩沖對象,索引緩沖對象,頂點數(shù)組對象,并綁定傳值;
        5. 鏈接頂點屬性;
        6. 創(chuàng)建和綁定紋理對象,加載圖片,傳遞紋理像素值;
        7. 讓程序進入渲染循環(huán),在循環(huán)中綁定頂點數(shù)組對象,不斷繪制圖形基元。

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

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

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

        1. 自定義材質(zhì)(材質(zhì)負責著色器程序);
        2. 自定義 Assembler (Assembler 負責傳遞頂點屬性);
        3. 設(shè)置材質(zhì)動態(tài)參數(shù),如設(shè)置紋理,變換平移旋轉(zhuǎn)縮放矩陣等。

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

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

        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?????????????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?????????????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 步,設(shè)置材質(zhì)動態(tài)參數(shù),在視頻播放器中,需要動態(tài)修改的就是紋理數(shù)據(jù)了,在移動端,ffplay 改造后的 AVPlayer 在播放過程,通過 ITextureRenderer.render(uint8_t) 接口調(diào)用到 void Video::setImage(const uint8_t *data) 方法,實際在不斷更新紋理數(shù)據(jù),代碼如下:

        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);
        ????}
        };

        至此,視頻展示小節(jié)完畢。

        4.4 音頻播放

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

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

        1. AudioTrack 屬于那種同步寫數(shù)據(jù)的方式,屬于 “推” 方案,google 最開始推行的方式,估計比較穩(wěn)定,由于 AudioTrack 是 Java 接口,C++ 調(diào)用需要反射,理論上,對效率有點影響。
        2. OpenSL ES 可以做到 “拉” 方案,但 google 在官方文檔說過對 OpenSL ES 接口沒做太多的兼容,也許不太可靠。

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

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

        音頻播放模型,屬于生產(chǎn)者消費者模型,音頻設(shè)備在開啟狀態(tài)下,會不斷拉取音頻解碼器生成的音頻樣本。

        音頻播放的接口并不復雜,主要用于替換 ffplay 程序中的 SDL 音頻相關(guān)接口,具體接口代碼如下:

        #ifndef?I_AUDIO_DEVICE_H
        #define?I_AUDIO_DEVICE_H

        #include?"AudioSpec.h"

        class?IAudioDevice?{

        public:

        ????virtual?~IAudioDevice()?{};

        ????virtual?bool?open(AudioSpec?*wantedSpec)?=?0;??//?開啟音頻設(shè)備,AudioSpec?結(jié)構(gòu)體包含拉取回調(diào)

        ????virtual?void?close()?=?0;??????????????????????//?關(guān)閉音頻輸出

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

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

        ????AudioSpec?spec;
        };

        #endif?//I_AUDIO_DEVICE_H

        4.5 優(yōu)化與擴展

        4.5.1 邊下邊播

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

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

        在查閱 FFmpeg 官方協(xié)議 文檔時,發(fā)現(xiàn)某些協(xié)議支持組合使用,如下:

        cache:http://host/resource

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

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

        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 方法中,調(diào)用其他協(xié)議的 read 方法,得到數(shù)據(jù)后,寫入文件并存儲下載信息,并把數(shù)據(jù)返回給播放器。

        4.5.2 libyuv 替換 swscale

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

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

        但在使用 swscale (已開啟 FFmpeg 編譯選項 neon 優(yōu)化)進行顏色編碼轉(zhuǎn)換后,就可以發(fā)現(xiàn) swscale 在移動端效率低下,使用小米 Mix 3 設(shè)備,1280x720 分辨率的視頻,像素格式從 AV_PIX_FMT_YUV420P 轉(zhuǎn)成 AV_PIX_FMT_RGB24,縮放按照二次線性采樣,平均耗時高達 16 毫秒,而且導致 CPU 占用率相當高。數(shù)據(jù)截圖待補:

        經(jīng)過 google 一番搜索,找到了 google 的 libyuv 替代方案

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

        官方優(yōu)化說明:

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

        使用 libyuv 進行像素格式轉(zhuǎn)換后,使用小米 Mix 3 設(shè)備,1280x720 分辨率的視頻,像素格式從 AV_PIX_FMT_YUV420P 轉(zhuǎn)成 AV_PIX_FMT_RGB24,縮放按照二次線性采樣,平均耗時 8 毫秒,相對 swscale 降低了一半。

        4.5.3 Android asset 協(xié)議

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

        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. 參考文檔

        1. FFmpeg: https://ffmpeg.org/
        2. Cocos Creator 自定義 Assembler: https://docs.cocos.com/creator/manual/zh/advanced-topics/custom-render.html#
        3. Cocos Creator JSB 綁定:https://docs.cocos.com/creator/manual/zh/advanced-topics/JSB2.0-learning.html
        4. Android Oboe: https://github.com/google/oboe/blob/master/docs/FullGuide.md
        5. Google libyuv: https://chromium.googlesource.com/libyuv/libyuv/+/HEAD/docs/getting_started.md
        6. LearnOpenGL CN: https://learnopengl-cn.github.io/
        7. WebGL: https://developer.mozilla.org/zh-CN/docs/Web/API/WebGL_API/Tutorial/Animating_textures_in_WebGL
        8. ijkplayer: https://github.com/bilibili/ijkplayer

        瀏覽 488
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            3d怪物性做爰免费视频 | 久久精品久久久久久久 | 天天综合网日日夜夜 | 色综合色色色 | h乳喷奶水被揉得受不了小说 | 无码人妻丰满熟妇区蜜桃 | 草草爱影院 | 免费无遮挡男女视频 | 亚洲成人一区 | 欧美三级电影在线 |