FFmpeg入門 - Android移植

系列文章:

  1. FFmpeg入門 - 視頻播放
  2. FFmpeg入門 - rtmp推流
  3. FFmpeg入門 - Android移植
  4. FFmpeg入門 - 格式轉(zhuǎn)換

前兩篇文章介紹了如何使用ffmpeg推流和拉流,這篇我們來看看怎樣將之前的代碼移植到安卓上。

FFmpeg編譯與集成

FFmpeg的安卓交叉編譯網(wǎng)上有很多的資料,基本上都是些編譯配置而已。可以直接將我的腳本放到ffmpeg源碼根目錄,修改下NDK的路徑和想要編譯的ABI之后直接執(zhí)行。然后就能在android目錄里面得到編譯好的so和.h

如果的確編譯出現(xiàn)問題,也可以直接用我編出來的

將庫放到AndroidStudio工程的jniLibs目錄,將include目錄放到app/src/main/cpp下,然后修改CMakeLists.txt添加ffmpeg頭文件路徑、庫路徑、鏈接配置等:

cmake_minimum_required(VERSION 3.18.1)

project("ffmpegdemo")

add_library(ffmpegdemo SHARED ffmpeg_demo.cpp video_sender.cpp opengl_display.cpp egl_helper.cpp video_decoder.cpp)

find_library(log-lib log)

# 頭文件路徑
include_directories(${CMAKE_SOURCE_DIR}/include)

# ffmpeg庫依賴
add_library(avcodec SHARED IMPORTED)
set_target_properties(avcodec PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../../../jniLibs/${ANDROID_ABI}/libavcodec.so)

add_library(avfilter SHARED IMPORTED)
set_target_properties(avfilter PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../../../jniLibs/${ANDROID_ABI}/libavfilter.so)

add_library(avformat SHARED IMPORTED)
set_target_properties(avformat PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../../../jniLibs/${ANDROID_ABI}/libavformat.so)

add_library(avutil SHARED IMPORTED)
set_target_properties(avutil PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../../../jniLibs/${ANDROID_ABI}/libavutil.so)

add_library(swresample SHARED IMPORTED)
set_target_properties(swresample PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../../../jniLibs/${ANDROID_ABI}/libswresample.so)

add_library(swscale SHARED IMPORTED)
set_target_properties(swscale PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../../../jniLibs/${ANDROID_ABI}/libswscale.so)

target_link_libraries(
        ffmpegdemo

        # log
        ${log-lib}

        EGL
        GLESv2
        android

        # FFmpeg libs
        avcodec
        avfilter
        avformat
        avutil
        swresample
        swscale
)

這樣一套下來其實(shí)ffmpeg的安卓環(huán)境就整好了,我們把之前的video_sender.cppvideo_sender.h拷貝過來添加個jni的接口驗(yàn)證下推流:

// java
File file = new File(getFilesDir(), "video.flv");

try {
    InputStream is = getAssets().open("video.flv");
    OutputStream os = new FileOutputStream(file);
    FileUtils.copy(is, os);
} catch (Exception e) {
    Log.d("FFmpegDemo", "err", e);
}

new Thread(new Runnable() {
    @Override
    public void run() {
        send(file.getAbsolutePath(), "rtmp://" + SERVER_IP + "/live/livestream");
    }
}).start();
//jni
extern "C" JNIEXPORT void JNICALL
Java_me_linjw_demo_ffmpeg_MainActivity_send(
        JNIEnv *env,
        jobject /* this */,
        jstring srcFile,
        jstring destUrl) {
    const char *src = env->GetStringUTFChars(srcFile, NULL);
    const char *dest = env->GetStringUTFChars(destUrl, NULL);
    LOGD("send: %s -> %s", src, dest);
    VideoSender::Send(src, dest);
}

然后就可以用安卓去推流,在pc上用之前的demo進(jìn)行播放驗(yàn)證。

OpenGLES播放FFmpeg

之前的demo使用SDL2播放視頻,但是安卓上更常規(guī)的做法是通過OpenGLES去播放。其實(shí)之前在做攝像教程的時候已經(jīng)有介紹過OpenGLES的使用了:

安卓特效相機(jī)(二) EGL基礎(chǔ)

安卓特效相機(jī)(三) OpenGL ES 特效渲染

這篇我們就只補(bǔ)充下之前沒有提到的部分。

YUV

首先有個很重要的知識點(diǎn)在于我們的視頻很多情況下解碼出來都是YUV格式的畫面而不是安卓應(yīng)用開發(fā)常見的RGB格式。

YUV是編譯true-color顏色空間(color space)的種類,Y'UV, YUV, YCbCr,YPbPr等專有名詞都可以稱為YUV,彼此有重疊。“Y”表示明亮度(Luminance、Luma),“U”和“V”則是色度、濃度(Chrominance、Chroma),也就是說通過UV可以選擇到一種顏色:

1.png

然后再加上這種顏色的亮度就能代表我們實(shí)際看到的顏色。

YUV的發(fā)明是由于彩色電視與黑白電視的過渡時期,黑白電視只有亮度的值(Y)到了彩色電視的時代為了兼容之前的黑白電視,于是在亮度值后面加上了UV值指定顏色,如果忽略了UV那么剩下的Y,就和黑白電視的信號保持一致。

這種情況下數(shù)據(jù)是以 平面格式(planar formats) 去保存的,類似YYYYUUUUVVVV,YUV三者分開存放。
另外也有和常見的RGB存放方式類似的 緊縮格式(packed formats) ,類似YUVYUVYUV,每個像素點(diǎn)的YUV數(shù)據(jù)連續(xù)存放。

由于人的肉眼對亮度敏感對顏色相對不敏感,所以我們可以相鄰的幾個像素共用用UV信息,減少數(shù)據(jù)帶寬。

這里的共用UV信息并沒有對多個像素點(diǎn)做UV數(shù)據(jù)的均值,而是簡單的跳過一些像素點(diǎn)不去讀取他們的UV數(shù)據(jù)。

YUV444

每個像素都有自己的YUV數(shù)據(jù),每個像素占用Y + U + V = 8 + 8 + 8 = 24 bits

YUV444.png

444的含義是同一行相鄰的4個像素,分別采樣4個Y,4個U,4個V

YUV422

每兩個像素共用一對UV分量,每像素平均占用Y + U + V = 8 + 4 + 4 = 16 bits

YUV422.png

422的含義是同一行相鄰的4個像素,分別采樣4個Y,2個U,2個V

YUV420

每四個像素共用一對UV分量,每像素平均占用Y + U + V = 8 + 2 + 2 = 12 bits

YUV420.png

YUV420在YUV422的基礎(chǔ)上再隔行掃描UV信息,一行只采集U,下一行只采集V

420的含義是同一行相鄰的4個像素,分別采樣4個Y,2個U,0個V,或者4個Y,0個U,2個V

OpenGLES顯示YUV圖像

由于OpenGLES使用RGB色彩,所以我們需要在fragmentShader里面將YUV轉(zhuǎn)成RGB,轉(zhuǎn)換公式如下:

R = Y + 1.4075 * V;
G = Y - 0.3455 * U - 0.7169*V;
B = Y + 1.779 * U;

由于解碼之后的數(shù)據(jù)使用平面格式(planar formats)保存,所以我們可以創(chuàng)建三張灰度圖圖片分別存儲YUV的分量,另外由于OpenGLES里面色彩的值范圍是0~1.0,而UV分量的取值范圍是-0.5~0.5所以我們UV分量統(tǒng)一減去0.5做偏移.于是fragmentShader代碼如下:

static const string FRAGMENT_SHADER = "#extension GL_OES_EGL_image_external : require\n"
                                      "precision highp float;\n"
                                      "varying vec2 vCoord;\n"
                                      "uniform sampler2D texY;\n"
                                      "uniform sampler2D texU;\n"
                                      "uniform sampler2D texV;\n"
                                      "varying vec4 vColor;\n"
                                      "void main() {\n"
                                      "    float y = texture2D(texY, vCoord).x;\n"
                                      "    float u = texture2D(texU, vCoord).x - 0.5;\n"
                                      "    float v = texture2D(texV, vCoord).x - 0.5;\n"
                                      "    float r = y + 1.4075 * v;\n"
                                      "    float g = y - 0.3455 * u - 0.7169 * v;\n"
                                      "    float b = y + 1.779 * u;\n"
                                      "    gl_FragColor = vec4(r, g, b, 1);\n"
                                      "}";

接著由于OpenGLES里面紋理坐標(biāo)原點(diǎn)是左下角,而解碼的畫面原點(diǎn)是左上角,所以紋理坐標(biāo)需要上下調(diào)換一下:

static const float VERTICES[] = {
        -1.0f, 1.0f,
        -1.0f, -1.0f,
        1.0f, -1.0f,
        1.0f, 1.0f
};

// 由于OpenGLES里面紋理坐標(biāo)原點(diǎn)是左下角,而解碼的畫面原點(diǎn)是左上角,所以紋理坐標(biāo)需要上下調(diào)換一下
static const float TEXTURE_COORDS[] = {
        0.0f, 0.0f,
        0.0f, 1.0f,
        1.0f, 1.0f,
        1.0f, 0.0f
};

static const short ORDERS[] = {
        0, 1, 2, // 左下角三角形

        2, 3, 0  // 右上角三角形
};

最后就只要將每幀解析出來的圖像交給OpenGLES去渲染就好:

AVFrame *frame;
while ((frame = decoder.NextFrame()) != NULL) {
    eglHelper.MakeCurrent();
    display.Render(frame->data, frame->linesize);
    eglHelper.SwapBuffers();
}

linesize

接著我們就需要根據(jù)這些YUV數(shù)據(jù)創(chuàng)建三個灰度圖分別存儲各個分量的數(shù)據(jù)。這里有個知識點(diǎn),解碼得到的YUV數(shù)據(jù),高是對應(yīng)分量的高,但是寬卻不一定是對應(yīng)分量的寬.

這是因?yàn)樵谧鲆曨l解碼的時候會對寬進(jìn)行對齊,讓寬是16或者32的整數(shù)倍,具體是16還是32由cpu決定.例如我們的video.flv視頻,原始畫面尺寸是289*160,如果按32去對齊的話,他的Y分量的寬則是320.

對齊之后的寬在ffmpeg里面稱為linesize,而由于我們這個demo只支持YUV420的格式,它的Y分量的高度為原始圖像的高度,UV分量的高度由于是隔行掃描,所以是原生圖像高度的一半:

void OpenGlDisplay::Render(uint8_t *yuv420Data[3], int lineSize[3]) {
    // 解碼得到的YUV數(shù)據(jù),高是對應(yīng)分量的高,但是寬卻不一定是對應(yīng)分量的寬
    // 這是因?yàn)樵谧鲆曨l解碼的時候會對寬進(jìn)行對齊,讓寬是16或者32的整數(shù)倍,具體是16還是32由cpu決定
    // 例如我們的video.flv視頻,原始畫面尺寸是689x405,如果按32去對齊的話,他的Y分量的寬則是720
    // 對齊之后的寬在ffmpeg里面稱為linesize
    // 而對于YUV420來說Y分量的高度為原始圖像的高度,UV分量的高度由于是隔行掃描,所以是原生圖像高度的一半
    setTexture(0, "texY", yuv420Data[0], lineSize[0], mVideoHeight);
    setTexture(1, "texU", yuv420Data[1], lineSize[1], mVideoHeight / 2);
    setTexture(2, "texV", yuv420Data[2], lineSize[2], mVideoHeight / 2);

    // 由于對齊之后創(chuàng)建的紋理寬度大于原始畫面的寬度,所以如果直接顯示,視頻的右側(cè)會出現(xiàn)異常
    // 所以我們將紋理坐標(biāo)進(jìn)行縮放,忽略掉右邊對齊多出來的部分
    GLint scaleX = glGetAttribLocation(mProgram, "aCoordScaleX");
    glVertexAttrib1f(scaleX, mVideoWidth * 1.0f / lineSize[0]);

    glClear(GL_DEPTH_BUFFER_BIT | GL_COLOR_BUFFER_BIT);
    glDrawElements(GL_TRIANGLES, sizeof(ORDERS) / sizeof(short), GL_UNSIGNED_SHORT, ORDERS);
}

另外由于對齊之后創(chuàng)建的紋理寬度大于原始畫面的寬度,所以如果直接顯示,視頻的右側(cè)會出現(xiàn)異常:

2.png

所以我們將紋理坐標(biāo)進(jìn)行縮放,忽略掉右邊對齊多出來的部分:

// VERTICES_SHADER
vCoord = vec2(aCoord.x * aCoordScaleX, aCoord.y);

保持視頻長寬比

雖然視頻能正常播放了,但是可以看到整個視頻是鋪滿屏幕的。所以我們需要對視頻進(jìn)行縮放讓他保持長寬比然后屏幕居中:

void OpenGlDisplay::SetVideoSize(int videoWidth, int videoHeight) {
    mVideoWidth = videoWidth;
    mVideoHeight = videoHeight;

    // 如果不做處理(-1.0f, 1.0f),(-1.0f, -1.0f),(1.0f, -1.0f),(1.0f, 1.0f)這個矩形會鋪滿整個屏幕導(dǎo)致圖像拉伸
    // 由于坐標(biāo)的原點(diǎn)在屏幕中央,所以只需要判斷是橫屏還是豎屏然后對x軸或者y軸做縮放就能讓圖像屏幕居中,然后恢復(fù)原始視頻的長寬比
    if (mWindowHeight > mWindowWidth) {
        // 如果是豎屏的話,圖像的寬不需要縮放,圖像的高縮小使其豎直居中
        GLint scaleX = glGetAttribLocation(mProgram, "aPosScaleX");
        glVertexAttrib1f(scaleX, 1.0f);

        // y坐標(biāo) * mWindowWidth / mWindowHeight 得到屏幕居中的正方形
        // 然后再 * videoHeight / videoWidth 就能恢復(fù)原始視頻的長寬比
        float r = 1.0f * mWindowWidth / mWindowHeight * videoHeight / videoWidth;
        GLint scaleY = glGetAttribLocation(mProgram, "aPosScaleY");
        glVertexAttrib1f(scaleY, r);
    } else {
        // 如果是橫屏的話,圖像的高不需要縮放,圖像的寬縮小使其水平居中
        GLint scaleY = glGetAttribLocation(mProgram, "aPosScaleY");
        glVertexAttrib1f(scaleY, 1.0f);

        // x坐標(biāo) * mWindowHeight / mWindowWidth 得到屏幕居中的正方形
        // 然后再 * videoWidth / videoHeight 就能恢復(fù)原始視頻的長寬比
        float r = 1.0f * mWindowHeight / mWindowWidth * videoWidth / videoHeight;
        GLint scaleX = glGetAttribLocation(mProgram, "aPosScaleX");
        glVertexAttrib1f(scaleX, r);
    }
}
// VERTICES_SHADER
gl_Position = vec4(aPosition.x * aPosScaleX, aPosition.y * aPosScaleY, 0, 1);
3.jpeg

Demo工程

完整的代碼已經(jīng)上傳到Github

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,156評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,401評論 3 415
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,069評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,873評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,635評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,128評論 1 323
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,203評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,365評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,881評論 1 334
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,733評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,935評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,475評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,172評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,582評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,821評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,595評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,908評論 2 372

推薦閱讀更多精彩內(nèi)容