image.png
項目位置 https://github.com/deepsadness/SDLCmakeDemo
將編譯好的FFmpeg集成進來。
- 編譯FFmpeg FFMpeg編譯部分的內容可以看之前的文章
- CMakeList 編寫
# 添加FFMpeg set(FFMPEG_INCLUDE ${CMAKE_SOURCE_DIR}/libs/ffmpeg/include) set(LINK_DIR ${CMAKE_SOURCE_DIR}/libs/${ANDROID_ABI}) include_directories(${FFMPEG_INCLUDE}) add_library(ffmpeg SHARED IMPORTED) set_target_properties(ffmpeg PROPERTIES IMPORTED_LOCATION ${LINK_DIR}/libffmpeg.so) #並把FFmpeg放到鏈接庫內 target_link_libraries( # Specifies the target library. main SDL2 GLESv1_CM GLESv2 ffmpeg # Links the target library to the log library # included in the NDK. ${log-lib})
- 檢查是否集成成功 修改native-lib.cpp 導入頭文件
image.png
修改main方法,打印FFMpeg的編譯信息
打印FFMpeg的編譯信息.png
運行後,查看編譯信息
屏幕快照 2018-11-13 上午11.59.17.png
說明我們集成成功了~~
FFmpeg+SDL2簡單的播放器。
視頻路徑參數傳遞
簡單的通過main方法來傳遞參數。 0. 修改java方法,給main函數傳遞參數
SDLActivity中getArguments.png
在SDLMain的Run方法中,會去將參數傳遞過去
image.png
1. 確定main方法傳遞過來的參數
SDL_android.c中對應的nativeRunMain方法.png
在SDL_android.c
中可以看到,我們傳遞的main方法中得到的第一個參數,都是app_process,
第二個開始纔是我們的參數。
因爲我們只傳遞一個參數,所以可以直接取到。
取到我們傳遞的video_path.png
FFmpeg+SDL2播放流程
FFmpeg+SDL2播放流程.png
SDL的運行流程
1. SDL_Init() 通過SDL_Init 我們傳入的flag來初始化SDL的各個子系統。我們這裏只是簡單的視頻播放,所以只初始化了video的部分。SDL當中還有其他的子系統。比如音頻。
SDL_Init(SDL_INIT_VIDEO)
2. SDL_CreateWindow()
- 通過SDL_CreateWindow來創建一個SDL_window對象。
//創建窗口 位置是中間。大小是0 ,SDL創建窗口的時候,大小都是0 window = SDL_CreateWindow("SDL_Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, pCodecCtx->width, pCodecCtx->height, SDL_WINDOW_RESIZABLE|SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL);
最後一個參數是flag.這樣代表的意思是,可以重新獲取尺寸的,全屏幕的,使用OPENGL的。
- SDL_Window表示SDL顯示的窗口。 這裏其實在Android中,如Flag所示,是通過創建一個NativeWindow,創建了一個OpenGL Surface進行繪製。
3. SDL_CreateRenderer SDL_Renderer負責SDL渲染的相關方法。
//-1 表示使用默認的窗口id 0是這是flag renderer = SDL_CreateRenderer(window, -1, 0);
後續的渲染循環,都需要用它來完成。
4.SDL_CreateTexture
- 創建一個SDL_Texture
SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width, pCodecCtx->height);
需要制定像素的格式SDL_PIXELFORMAT_YV12
,對應的就是YUV420P
;
接收的頻率,SDL_TEXTUREACCESS_STREAMING
這個表示會被頻繁刷新。
- SDL_Texture,用來接收傳入的數據。
FFmpeg的運行流程
FFmpeg運行的流程。 簡單來說就是
- 獲取
AVFormatContext
,這個變量內包含了IO的相關數據 通過avformat_open_input
方法獲取
//創建avformat AVFormatContext *pFormatCtx = avformat_alloc_context(); ret = avformat_open_input(&pFormatCtx, video_path, NULL, NULL);
- 獲取
AVCodecContext
,這個變量內包含了編碼器或者解碼器的相關數據。 它的獲取需要,先從AVFormatContext
中取到對應的流,根據對應的流的信息得到對應的編碼器或者解碼器AVCodec
。然後再來創建。
// 先去找到video_stream,然後在找AVCodec //先檢查一邊 ret = avformat_find_stream_info(pFormatCtx, NULL); if (ret < 0) { ALOGE("Can not find Stream info!!!"); return avError(ret); } int video_stream = -1; //這裏就是簡單的直接去找視頻流 for (int i = 0; i < pFormatCtx->nb_streams; ++i) { if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { video_stream = i; break; } } if (video_stream == -1) { ALOGE("Can not find video stream!!!"); return -1; } ALOGI("find video stream ,index = %d", video_stream); //創建AVCodecCtx //需要先去獲得AVCodec AVCodec *pCodec = avcodec_find_decoder(pFormatCtx->streams[video_stream]->codecpar->codec_id); if (pCodec == NULL) { ALOGE("Can not find video decoder!!!"); return -1; } //成功獲取上下文。獲取之後,需要對上下文的部分內容進行初始化 AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec); //將解碼器的參數複製過去 AVCodecParameters *codecParameters = pFormatCtx->streams[video_stream]->codecpar; ret = avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[video_stream]->codecpar); if (ret < 0) { ALOGE("avcodec_parameters_from_context error!!"); return avError(ret); } ret = avcodec_open2(pCodecCtx, pCodec, NULL);
- 解碼的時候,通過
av_read_frame
進行讀取。 通過avcodec_send_packet
和avcodec_receive_frame
不斷進行編碼和解碼。 用AVPacket
接收壓縮的數據(編碼後,解碼前)。用AVFrame
接收原始的YUV數據(編碼前,解碼後)
代碼
extern "C" //這裏是直接定義了SDL的main方法嗎 int main(int argc, char *argv[]) { // 打印ffmpeg信息 const char *str = avcodec_configuration(); ALOGI("avcodec_configuration: %s", str); char *video_path = argv[1]; ALOGI("video_path : %s", video_path); //開始ffmpeg註冊的流程 int ret = 0; //重定向log av_log_set_callback(syslog_print); //註冊 av_register_all(); avcodec_register_all(); //創建avformat AVFormatContext *pFormatCtx = avformat_alloc_context(); ret = avformat_open_input(&pFormatCtx, video_path, NULL, NULL); if (ret < 0) { ALOGE("avformat open input failed!"); return avError(ret); } //輸出avformat av_dump_format(pFormatCtx, -1, video_path, 0); // 先去找到video_stream,然後在找AVCodec //先檢查一邊 ret = avformat_find_stream_info(pFormatCtx, NULL); if (ret < 0) { ALOGE("Can not find Stream info!!!"); return avError(ret); } int video_stream = -1; //這裏就是簡單的直接去找視頻流 for (int i = 0; i < pFormatCtx->nb_streams; ++i) { if (pFormatCtx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) { video_stream = i; break; } } if (video_stream == -1) { ALOGE("Can not find video stream!!!"); return -1; } ALOGI("find video stream ,index = %d", video_stream); //創建AVCodecCtx //需要先去獲得AVCodec AVCodec *pCodec = avcodec_find_decoder(pFormatCtx->streams[video_stream]->codecpar->codec_id); if (pCodec == NULL) { ALOGE("Can not find video decoder!!!"); return -1; } //成功獲取上下文。獲取之後,需要對上下文的部分內容進行初始化 AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec); //將解碼器的參數複製過去 AVCodecParameters *codecParameters = pFormatCtx->streams[video_stream]->codecpar; ret = avcodec_parameters_to_context(pCodecCtx, pFormatCtx->streams[video_stream]->codecpar); if (ret < 0) { ALOGE("avcodec_parameters_from_context error!!"); return avError(ret); } AVDictionaryEntry *t = NULL; while ((t = av_dict_get(pFormatCtx->metadata, "", t, AV_DICT_IGNORE_SUFFIX))) { char *key = t->key; char *value = t->value; ALOGI("key = %s,value = %s", key, value); } int height = codecParameters->height; int width = codecParameters->width; ALOGI("width = %d,height = %d", width, height); //完成初始化的參數之後,就要打開解碼器,準備解碼啦!! ret = avcodec_open2(pCodecCtx, pCodec, NULL); if (ret < 0) { ALOGE("avcodec_open2 error!!"); return avError(ret); } ALOGI("w = %d,h = %d", pCodecCtx->width, pCodecCtx->height); //解碼,就對應了 解碼器前的數據,壓縮數據 AVPacket 解碼後的數據 AVFrame 就是我們需要的YUV數據 //先給AVFrame分配內存空間 AVFrame *pFrameYUV = av_frame_alloc(); //pCodecCtx->pix_fmt == AV_PIX_FMT_YUV420P?? int buffer_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1); uint8_t *buffers = (uint8_t *) av_malloc(buffer_size); //將buffers 的地址賦給 AVFrame av_image_fill_arrays(pFrameYUV->data, pFrameYUV->linesize, buffers, AV_PIX_FMT_YUV420P, pCodecCtx->width, pCodecCtx->height, 1); //開始準備sdl的部分 //SDL 四大要 window render texture surface SDL_Window *window; SDL_Renderer *renderer; SDL_Event event; SDL_Rect sdlRect; SDL_Thread *video_tid; //初始化SDL if (SDL_Init(SDL_INIT_VIDEO) < 0) { ALOGE("Could not initialize SDL - %s", SDL_GetError()); return 1; } //創建窗口 位置是中間。大小是0 ,SDL創建窗口的時候,大小都是0 window = SDL_CreateWindow("SDL_Window", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, pCodecCtx->width, pCodecCtx->height, SDL_WINDOW_RESIZABLE | SDL_WINDOW_FULLSCREEN | SDL_WINDOW_OPENGL); if (!window) { ALOGE("SDL:could not set video mode -exiting!!!\n"); return -1; } //創建Renderer -1 表示使用默認的窗口 後面一個是Renderer的方式,0的話,應該就是未指定把??? renderer = SDL_CreateRenderer(window, -1, 0); //這裏的YU12 對應YUV420P ,SDL_TEXTUREACCESS_STREAMING 是表示texture 是不斷被刷新的。 SDL_Texture *texture = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_YV12, SDL_TEXTUREACCESS_STREAMING, pCodecCtx->width, pCodecCtx->height); // 設置顯示的大小 sdlRect.x = 0; sdlRect.y = 0; sdlRect.w = pCodecCtx->width; sdlRect.h = pCodecCtx->height; //準備好了Window 開始準備解碼的數據 AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket)); // video_tid int yuv_width = pCodecCtx->width * pCodecCtx->height; av_new_packet(packet, yuv_width); //當你需要對齊進行縮放和轉化的時候,需要先申請一個SwsContext SwsContext *img_convert = sws_getContext(pCodecCtx->width, pCodecCtx->height, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height, AV_PIX_FMT_YUV420P, SWS_BICUBIC, NULL, NULL, NULL); while (av_read_frame(pFormatCtx, packet) >= 0) { if (packet->stream_index == video_stream) { //送入解碼器 int gop = avcodec_send_packet(pCodecCtx, packet); //如果成功獲取一幀的數據 if (gop == 0) { //使用pFrame接受數據 ret = avcodec_receive_frame(pCodecCtx, pFrameYUV); if (ret == 0) { //進行縮放。這裏可以用libyuv進行轉換 sws_scale(img_convert, reinterpret_cast<const uint8_t *const *>(pFrameYUV ->data), pFrameYUV->linesize, 0, pCodecCtx->height, pFrameYUV->data, pFrameYUV->linesize); //應爲是YUV,所以調用UpdateYUV方法,分別將YUV填充進去 SDL_UpdateYUVTexture(texture, &sdlRect, pFrameYUV ->data[0], pFrameYUV->linesize[0], pFrameYUV->data[1], pFrameYUV->linesize[1], pFrameYUV->data[2], pFrameYUV->linesize[2]); //清空數據 SDL_RenderClear(renderer); //複製數據 SDL_RenderCopy(renderer, texture, &sdlRect, &sdlRect ); //渲染到屏幕 SDL_RenderPresent(renderer); //延遲40 25 fps??? Android端使用的話,就會卡頓 // SDL_Delay(40); } else if (ret == AVERROR(EAGAIN)) { ALOGE("%s", "Frame is not available right, please try another input"); } else if (ret == AVERROR_EOF) { ALOGE("%s", "the decoder has been fully flushed"); } else if (ret == AVERROR(EINVAL)) { ALOGE("%s", "codec not opened, or it is an encoder"); } else { ALOGI("%s", "legitimate decoding errors"); } } } //讀完,再次釋放這個pack,重新去讀 av_packet_unref(packet); //每一幀,去相應一次對應的SDL事件 if (SDL_PollEvent(&event)) { SDL_bool needToQuit = SDL_FALSE; switch (event.type) { case SDL_QUIT: case SDL_KEYDOWN: needToQuit = SDL_TRUE; break; default: break; } if (needToQuit) { break; } } } //SDL資源釋放 SDL_DestroyTexture(texture); SDL_DestroyRenderer(renderer); SDL_DestroyWindow(window); SDL_Quit(); //FFmpeg資源釋放 sws_freeContext(img_convert); av_free(buffers); av_free(pFrameYUV); avcodec_parameters_free(&pFormatCtx ->streams[video_stream]->codecpar); avcodec_close(pCodecCtx); avformat_close_input(&pFormatCtx); return 0; }
疑問
需要注意的是:在Android上不能使用SDL_Delay(); 在其他平臺上視乎是要使用SDL_Delay(40);才能保持幀率,但是Android上,好像不能使用?這個是爲什麼?
參考
最簡單的基於FFMPEG+SDL的視頻播放器 ver2 (採用SDL2.0) FFmpeg編程開發筆記 —— Android FFmpeg + SDL2.0簡易播放器實現