
項目位置 https://github.com/deepsadness/SDLCmakeDemo
系列內(nèi)容導讀
- SDL2-移植Android Studio+CMakeList集成
- Android端FFmpeg +SDL2的簡單播放器
- SDL2 Android端的簡要分析(VideoSubSystem)
- SDL2 Android端的簡要分析(AudioSubSystem)
將編譯好的FFmpeg集成進來。
- 編譯FFmpeg
FFMpeg編譯部分的內(nèi)容可以看之前的文章
偶遇FFmpeg(番外)——FFmpeg花樣編譯入魔1之裁剪大小
偶遇FFmpeg(番外)——FFmpeg花樣編譯入魔2之單個SO庫和ndk15之后
偶遇FFmpeg(三)——Android集成
- 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放到鏈接庫內(nèi)
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+SDL2簡單的播放器。
視頻路徑參數(shù)傳遞
簡單的通過main方法來傳遞參數(shù)。
0. 修改java方法,給main函數(shù)傳遞參數(shù)

在SDLMain的Run方法中,會去將參數(shù)傳遞過去

1. 確定main方法傳遞過來的參數(shù)

在
SDL_android.c中可以看到,我們傳遞的main方法中得到的第一個參數(shù),都是app_process,第二個開始才是我們的參數(shù)。
因為我們只傳遞一個參數(shù),所以可以直接取到。

FFmpeg+SDL2播放流程

SDL的運行流程
1. SDL_Init()
通過SDL_Init 我們傳入的flag來初始化SDL的各個子系統(tǒng)。我們這里只是簡單的視頻播放,所以只初始化了video的部分。SDL當中還有其他的子系統(tǒng)。比如音頻。
SDL_Init(SDL_INIT_VIDEO)
2. SDL_CreateWindow()
- 通過SDL_CreateWindow來創(chuàng)建一個SDL_window對象。
//創(chuàng)建窗口 位置是中間。大小是0 ,SDL創(chuàng)建窗口的時候,大小都是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);
最后一個參數(shù)是flag.這樣代表的意思是,可以重新獲取尺寸的,全屏幕的,使用OPENGL的。
- SDL_Window表示SDL顯示的窗口。
這里其實在Android中,如Flag所示,是通過創(chuàng)建一個NativeWindow,創(chuàng)建了一個OpenGL Surface進行繪制。
3. SDL_CreateRenderer
SDL_Renderer負責SDL渲染的相關方法。
//-1 表示使用默認的窗口id 0是這是flag
renderer = SDL_CreateRenderer(window, -1, 0);
后續(xù)的渲染循環(huán),都需要用它來完成。
4.SDL_CreateTexture
- 創(chuàng)建一個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,用來接收傳入的數(shù)據(jù)。
FFmpeg的運行流程
FFmpeg運行的流程。
簡單來說就是
- 獲取
AVFormatContext,這個變量內(nèi)包含了IO的相關數(shù)據(jù)
通過avformat_open_input方法獲取
//創(chuàng)建avformat
AVFormatContext *pFormatCtx = avformat_alloc_context();
ret = avformat_open_input(&pFormatCtx, video_path, NULL, NULL);
- 獲取
AVCodecContext,這個變量內(nèi)包含了編碼器或者解碼器的相關數(shù)據(jù)。
它的獲取需要,先從AVFormatContext中取到對應的流,根據(jù)對應的流的信息得到對應的編碼器或者解碼器AVCodec。然后再來創(chuàng)建。
// 先去找到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);
//創(chuàng)建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;
}
//成功獲取上下文。獲取之后,需要對上下文的部分內(nèi)容進行初始化
AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);
//將解碼器的參數(shù)復制過去
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接收壓縮的數(shù)據(jù)(編碼后,解碼前)。用AVFrame接收原始的YUV數(shù)據(jù)(編碼前,解碼后)
代碼
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();
//創(chuàng)建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);
//創(chuàng)建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;
}
//成功獲取上下文。獲取之后,需要對上下文的部分內(nèi)容進行初始化
AVCodecContext *pCodecCtx = avcodec_alloc_context3(pCodec);
//將解碼器的參數(shù)復制過去
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);
//完成初始化的參數(shù)之后,就要打開解碼器,準備解碼啦??!
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);
//解碼,就對應了 解碼器前的數(shù)據(jù),壓縮數(shù)據(jù) AVPacket 解碼后的數(shù)據(jù) AVFrame 就是我們需要的YUV數(shù)據(jù)
//先給AVFrame分配內(nèi)存空間
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;
}
//創(chuàng)建窗口 位置是中間。大小是0 ,SDL創(chuàng)建窗口的時候,大小都是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;
}
//創(chuàng)建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 開始準備解碼的數(shù)據(jù)
AVPacket *packet = (AVPacket *) av_malloc(sizeof(AVPacket));
// video_tid
int yuv_width = pCodecCtx->width * pCodecCtx->height;
av_new_packet(packet, yuv_width);
//當你需要對齊進行縮放和轉(zhuǎn)化的時候,需要先申請一個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);
//如果成功獲取一幀的數(shù)據(jù)
if (gop == 0) {
//使用pFrame接受數(shù)據(jù)
ret = avcodec_receive_frame(pCodecCtx, pFrameYUV);
if (ret == 0) {
//進行縮放。這里可以用libyuv進行轉(zhuǎn)換
sws_scale(img_convert, reinterpret_cast<const uint8_t *const *>(pFrameYUV
->data), pFrameYUV->linesize, 0,
pCodecCtx->height,
pFrameYUV->data, pFrameYUV->linesize);
//應為是YUV,所以調(diào)用UpdateYUV方法,分別將YUV填充進去
SDL_UpdateYUVTexture(texture, &sdlRect,
pFrameYUV
->data[0], pFrameYUV->linesize[0],
pFrameYUV->data[1], pFrameYUV->linesize[1],
pFrameYUV->data[2], pFrameYUV->linesize[2]);
//清空數(shù)據(jù)
SDL_RenderClear(renderer);
//復制數(shù)據(jù)
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編程開發(fā)筆記 —— Android FFmpeg + SDL2.0簡易播放器實現(xiàn)
