只為解決一個核心問題,追求更好體驗。
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 替換 swscaleYUV(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