【記錄】FFmpeg音視訊學習
1.編譯FFmpeg
編譯到Android則需要額外配置:
- 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)'
- 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
- 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播放音訊:
- 在C層呼叫Java層的createAudioTrack方法,建立AudioTrack物件。
- 然後在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.