1. 程式人生 > >【記錄】FFmpeg音視訊學習

【記錄】FFmpeg音視訊學習

1.編譯FFmpeg

編譯到Android則需要額外配置:

  1. 1、修改ffmpeg專案根目錄下的configure檔案

將檔案中的如下四行:
SLIBNAME_WITH_MAJOR='$(SLIBNAME).$(LIBMAJOR)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_VERSION)'
SLIB_INSTALL_LINKS='$(SLIBNAME_WITH_MAJOR)$(SLIBNAME)'


替換為:
SLIBNAME_WITH_MAJOR='$(SLIBPREF)$(FULLNAME)-$(LIBMAJOR)$(SLIBSUF)'
LIB_INSTALL_EXTRA_CMD='$$(RANLIB)"$(LIBDIR)/$(LIBNAME)"'
SLIB_INSTALL_NAME='$(SLIBNAME_WITH_MAJOR)'
SLIB_INSTALL_LINKS='$(SLIBNAME)'
 

  1. 2、編寫build_android.sh指令碼

指令碼內容中路徑相關的引數需要根據實際開發環境進行修改:

#!/bin/bash
NDK=/f/android-ndk-r10e
SYSROOT=$NDK/platforms/android-9/arch-arm/
TOOLCHAIN=$NDK/toolchains/arm-linux-androideabi-4.9/prebuilt/windows-x86_64

function build_one
{
./configure \
    --prefix=$PREFIX \
    --enable-shared \
    --disable-static \
    --disable-doc \--enable-cross-compile \
    --cross-prefix=$TOOLCHAIN/bin/arm-linux-androideabi- \
    --target-os=linux \
    --arch=arm \
    --sysroot=$SYSROOT \
    --extra-cflags="-Os -fpic $ADDI_CFLAGS" \
    --extra-ldflags="$ADDI_LDFLAGS" \
    $ADDITIONAL_CONFIGURE_FLAG

make clean
make -j8
make install

$TOOLCHAIN/bin/arm-linux-androideabi-ld \
-rpath-link=$PLATFORM/usr/lib \
-L$PLATFORM/usr/lib \
-L$PREFIX/lib \
-soname libffmpeg.so -shared -nostdlib -Bsymbolic --whole-archive --no-undefined -o \
$PREFIX/libffmpeg.so \
libavcodec/libavcodec.a \
libavfilter/libavfilter.a \
libswresample/libswresample.a \
libavformat/libavformat.a \
libavutil/libavutil.a \
libswscale/libswscale.a \
libavdevice/libavdevice.a \
-lc -lm -lz -ldl -llog --dynamic-linker=/system/bin/linker \
$TOOLCHAIN/lib/gcc/arm-linux-androideabi/4.9/libgcc.a

}

CPU=armeabi-v7a
PREFIX=./android/$CPU
ADDI_CFLAGS="-marm"
build_one

  1. 3、使用MSYS執行編譯指令碼

命令:./build_android.sh

編譯後會在當前目錄下生成一個命名為android的資料夾,開啟就能找到我們想要的so檔案了。

2.視訊解碼並播放

extern "C" JNIEXPORT void JNICALL
Java_com_dovar_ffmpeg_1so_MainActivity_decodeVideo(JNIEnv *env, jobject,
                                                   jstring videoPath, jobject surface) {
    const char *file_name = (*env).GetStringUTFChars(videoPath, JNI_FALSE);

    //註冊
    av_register_all();
    //如果是網路流,則需要初始化網路相關
    avformat_network_init();

    AVFormatContext *pFormatCtx = avformat_alloc_context();
    //開啟視訊檔案
    if (avformat_open_input(&pFormatCtx, file_name, NULL, NULL) != 0) {
        LOGD("Could not open file:%s\n", file_name);
        return;
    }

    //檢索流資訊
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        LOGD("Could not find stream information.\n");
        return;
    }

    //查詢視訊流,一個多媒體檔案中可能含有音訊流、視訊流、字幕流等
    int videoStream = -1;
    for (int i = 0; i < pFormatCtx->nb_streams; ++i) {
        if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoStream = i;
            break;
        }
    }
    if (videoStream == -1) {
        LOGD("Didn't find a video stream.\n");
        return;
    }

    //獲取解碼器
    AVCodec *pCodec = avcodec_find_decoder(pFormatCtx->streams[videoStream]->codecpar->codec_id);
    if (pCodec == NULL) {
        LOGD("Codec not found.\n");
        return;
    }
    //初始化解碼器上下文
    AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);
    avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[videoStream]->codecpar);
    //開啟解碼器
    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        LOGD("Could not open codec.\n");
    }

    //輸出視訊資訊
    LOGD("視訊的檔案格式:%s", pFormatCtx->iformat->name);
    LOGD("視訊時長:%d", static_cast<int>((pFormatCtx->duration) / 1000000));
    LOGD("視訊的寬高:%d,%d", pCodecCtx->width, pCodecCtx->height);
    LOGD("解碼器的名稱:%s", pCodec->name);

    LOGD("開始準備原生繪製工具")
    //獲取NativeWindow,用於渲染視訊
    ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);
    ANativeWindow_setBuffersGeometry(nativeWindow, pCodecCtx->width, pCodecCtx->height,
                                     WINDOW_FORMAT_RGBA_8888);
    //定義繪圖緩衝區
    ANativeWindow_Buffer windowBuffer;
    LOGD("原生繪製工具準備完成")

    /*** 轉碼相關BEGIN ***/
    AVFrame *pFrameOut = av_frame_alloc();
    if (pFrameOut == NULL) {
        LOGD("Could not allocate video frame.\n");
        return;
    }
    int num = av_image_get_buffer_size(AV_PIX_FMT_RGBA, pCodecCtx->width, pCodecCtx->height, 1);
    uint8_t *buffer = (uint8_t *) (av_malloc(num * sizeof(uint8_t)));
    av_image_fill_arrays(pFrameOut->data, pFrameOut->linesize, buffer, AV_PIX_FMT_RGBA,
                         pCodecCtx->width, pCodecCtx->height, 1);
    struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width, pCodecCtx->height,
                                                pCodecCtx->pix_fmt, pCodecCtx->width,
                                                pCodecCtx->height, AV_PIX_FMT_RGBA, SWS_BILINEAR,
                                                NULL, NULL, NULL);
    if (sws_ctx == NULL) {
        LOGD("sws_ctx==null\n");
        return;
    }
    /*** 轉碼相關END ***/

    AVFrame *pFrame = av_frame_alloc();
    AVPacket packet;
    //讀取幀資料
    while (av_read_frame(pFormatCtx, &packet) >= 0) {
        if (packet.stream_index == videoStream) {
            //解碼AVPacket->AVFrame
            //傳送讀取到的壓縮資料(每次傳送可能包含一幀或多幀資料)
            if (avcodec_send_packet(pCodecCtx, &packet) != 0) {
                continue;
            }

            //讀取到一幀視訊
            while (avcodec_receive_frame(pCodecCtx, pFrame) == 0) {
                //鎖定視窗繪圖介面
                ANativeWindow_lock(nativeWindow, &windowBuffer, 0);

                //執行轉碼
                sws_scale(sws_ctx, (uint8_t const *const *) pFrame->data, pFrame->linesize, 0,
                          pCodecCtx->height, pFrameOut->data, pFrameOut->linesize);

//                LOGD("轉碼完成,開始渲染資料.\n")
                //獲取stride
                uint8_t *dst = (uint8_t *) windowBuffer.bits;
                int dstStride = windowBuffer.stride * 4;
                uint8_t *src = pFrameOut->data[0];
                int srcStride = pFrameOut->linesize[0];
                //由於視窗的stride和幀的stride不同,因此需要逐行復制
                int h;
                for (h = 0; h < pCodecCtx->height; h++) {
                    memcpy(dst + h * dstStride, src + h * srcStride, (size_t) (srcStride));
                }

                //解鎖視窗
                ANativeWindow_unlockAndPost(nativeWindow);
                /*  //進行短暫休眠。如果休眠時間太長會導致播放的每幀畫面有延遲感,如果短會有加速播放的感覺。
                  //一般一每秒60幀——16毫秒一幀的時間進行休眠
                  usleep(1000 * 20);*/
            }
        }

        //重置packet
        av_packet_unref(&packet);
    }

    //回收資源
    //釋放影象幀
    av_frame_free(&pFrame);
    av_frame_free(&pFrameOut);
    av_free(buffer);
    //關閉轉碼上下文
    sws_freeContext(sws_ctx);
    //關閉解碼器
    avcodec_close(pCodecCtx);
    //關閉視訊檔案
    avformat_close_input(&pFormatCtx);
    //登出網路相關
    avformat_network_deinit();

    avformat_free_context(pFormatCtx);
}

3.音訊解碼並播放

這裡我使用AudioTrack播放音訊:

  1. 在C層呼叫Java層的createAudioTrack方法,建立AudioTrack物件。
  2. 然後在C層呼叫AudioTrack的play、write進行播放。

Java層程式碼:

C層程式碼native-lib.cpp:
//音訊解碼
extern "C" JNIEXPORT void JNICALL
Java_com_dovar_ffmpeg_1so_MainActivity_decodeAudio(JNIEnv *env, jobject obj,
                                                   jstring audioPath) {
    const char *file_name = (*env).GetStringUTFChars(audioPath, JNI_FALSE);
    //1.註冊元件
    av_register_all();
    //封裝格式上下文
    AVFormatContext *pFormatCtx = avformat_alloc_context();

    //2.開啟輸入音訊檔案
    if (avformat_open_input(&pFormatCtx, file_name, NULL, NULL) != 0) {
        LOGD("%s", "開啟輸入音訊檔案失敗");
        return;
    }
    //3.獲取音訊資訊
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        LOGD("%s", "獲取音訊資訊失敗");
        return;
    }

    //音訊解碼,需要找到對應的AVStream所在的pFormatCtx->streams的索引位置
    int audio_stream_idx = -1;
    int i = 0;
    for (; i < pFormatCtx->nb_streams; i++) {
        //根據型別判斷是否是音訊流
        if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
            audio_stream_idx = i;
            break;
        }
    }
    //4.獲取解碼器
    AVCodec *pCodec = avcodec_find_decoder(
            pFormatCtx->streams[audio_stream_idx]->codecpar->codec_id);
    //根據索引拿到對應的流,根據流拿到解碼器上下文
    AVCodecContext *pCodeCtx = avcodec_alloc_context3(pCodec);
    avcodec_parameters_to_context(pCodeCtx, pFormatCtx->streams[audio_stream_idx]->codecpar);
    if (pCodec == NULL) {
        LOGD("%s", "無法解碼");
        return;
    }
    //5.開啟解碼器
    if (avcodec_open2(pCodeCtx, pCodec, NULL) < 0) {
        LOGD("%s", "編碼器無法開啟");
        return;
    }

    //輸出視訊資訊
    LOGD("音訊的檔案格式:%s", pFormatCtx->iformat->name);
    LOGD("音訊時長:%d", static_cast<int>((pFormatCtx->duration) / 1000000));
    LOGD("音訊的寬高:%d,%d", pCodeCtx->width, pCodeCtx->height);
    LOGD("解碼器的名稱:%s", pCodec->name);

    //編碼資料
    AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
    //解壓縮資料
    AVFrame *frame = av_frame_alloc();

    //frame->16bit 44100 PCM 統一音訊取樣格式與取樣率
    SwrContext *swrCtx = swr_alloc();
    //重取樣設定選項-----------------------------------------------------------start
    //輸入的取樣格式
    enum AVSampleFormat in_sample_fmt = pCodeCtx->sample_fmt;
    //輸出的取樣格式 16bit PCM
    enum AVSampleFormat out_sample_fmt = AV_SAMPLE_FMT_S16;
    //輸入的取樣率
    int in_sample_rate = pCodeCtx->sample_rate;
    //輸出的取樣率
    int out_sample_rate = 44100;
    //輸入的聲道佈局
    uint64_t in_ch_layout = pCodeCtx->channel_layout;
    //輸出的聲道佈局
    uint64_t out_ch_layout = AV_CH_LAYOUT_MONO;

    swr_alloc_set_opts(swrCtx, out_ch_layout, out_sample_fmt, out_sample_rate, in_ch_layout,
                       in_sample_fmt,
                       in_sample_rate, 0, NULL);
    swr_init(swrCtx);
    //重取樣設定選項-----------------------------------------------------------end
    //獲取輸出的聲道個數
    int out_channel_nb = av_get_channel_layout_nb_channels(out_ch_layout);
    //儲存pcm資料
    uint8_t *out_buffer = (uint8_t *) av_malloc(2 * 44100);

    jclass player_class = (env)->GetObjectClass(obj);
    jmethodID audio_track_method = (*env).GetMethodID(player_class, "createAudioTrack",
                                                      "(II)Landroid/media/AudioTrack;");
    if (!audio_track_method) {
        LOGD("audio_track_method not found...")
    }
    jobject audio_track = (env)->CallObjectMethod(obj, audio_track_method, out_sample_rate,
                                                  out_channel_nb);
    //呼叫play方法
    jclass audio_track_class = (*env).GetObjectClass(audio_track);
    jmethodID audio_track_play_mid = (*env).GetMethodID(audio_track_class, "play", "()V");
    (*env).CallVoidMethod(audio_track, audio_track_play_mid);

    //獲取write()方法
    jmethodID audio_track_write_mid = (*env).GetMethodID(audio_track_class, "write", "([BII)I");

    int framecount = 0;
    //6.一幀一幀讀取壓縮的音訊資料AVPacket
    while (av_read_frame(pFormatCtx, packet) >= 0) {
        if (packet->stream_index == audio_stream_idx) {
            //解碼AVPacket->AVFrame
            //傳送壓縮資料
            if (avcodec_send_packet(pCodeCtx, packet) != 0) {
                LOGD("%s", "解碼錯誤");
                continue;
            }

            //讀取到一幀音訊或者視訊
            while (avcodec_receive_frame(pCodeCtx, frame) == 0) {
                LOGD("解碼%d幀", framecount++);
                swr_convert(swrCtx, &out_buffer, 2 * 44100,
                            (const uint8_t **) (frame->data), frame->nb_samples);
                //獲取sample的size
                int out_buffer_size = av_samples_get_buffer_size(NULL, out_channel_nb,
                                                                 frame->nb_samples,
                                                                 out_sample_fmt, 1);

                jbyteArray audio_sample_array = (*env).NewByteArray(out_buffer_size);
                jbyte *sample_byte_array = (*env).GetByteArrayElements(audio_sample_array, NULL);
                //拷貝緩衝資料
                memcpy(sample_byte_array, out_buffer, (size_t) out_buffer_size);
                //釋放陣列
                (*env).ReleaseByteArrayElements(audio_sample_array, sample_byte_array, 0);
                //呼叫AudioTrack的write方法進行播放
                (*env).CallIntMethod(audio_track, audio_track_write_mid,
                                     audio_sample_array, 0, out_buffer_size);
                //釋放區域性引用
                (*env).DeleteLocalRef(audio_sample_array);
                usleep(1000 * 16);
            }
        }
        av_packet_unref(packet);
    }
    av_frame_free(&frame);
    av_free(out_buffer);
    swr_free(&swrCtx);
    avcodec_close(pCodeCtx);
    avformat_close_input(&pFormatCtx);
}

4.音視訊同步

PTS:Presentation Time Stamp。PTS主要用於度量解碼後的視訊幀什麼時候被顯示出來。

time_base:時間基。如果把1秒分為25等份,那麼每一格表示的就是1/25秒,此時的time_base={1,25}。

duration=pts*time_base.