之前已經(jīng)把FFmpeg集成到項目里面了,剩下的就是做開發(fā)了,做過安卓視頻播放的都應該知道在播放的時候都有用到SurfaceView,這里我們也采用這種方式。
一、定義Java層的調(diào)用接口
- 我們需要知道播放視頻的網(wǎng)絡地址或者是本地路徑,并且希望這個地址是可以修改的,所以我們需要有一個參數(shù)去接收這個地址。
- 和系統(tǒng)一樣,我們也需要傳遞一個Surface,在Jni中沒有Surface這個類型,所以要用Object(JNI中除了基本的數(shù)據(jù)類型,其他的都用Object)。
- 開始播放,解碼并進行播放視頻
所以定義看三個方法,第一個有返回值的,返回小于1的初始化失敗,正常返回0。這個可以自行修改。
public class PlayerCore {
/**
* 設置播放路徑
*
* @param path
*/
public native int init(String path);
/**
* 設置播放渲染的surface
*
* @param surface
*/
public native void setSurface(Object surface);
/**
* 開始播放
*/
public native void start();
}
二、功能實現(xiàn)
1、個人理解的NDK
這個純屬個人理解,不對之處還請見諒,望指正。
我把NDK開發(fā)分為Java、JNI和C/C++層。
- Java層。這個就不說了。
- JNI層:與Java層相對應的一層,是連接Java和C/C++的一個橋梁,這一層,必不可少。
- C/C++層:自己寫的或者是別人寫好的C/C++源碼或者是庫。
2、實現(xiàn)思路
2.1、Java傳遞給JNI路徑,JNI轉(zhuǎn)換成C/C++的路徑傳遞給解碼器,即:
String--->jstring--->const char *
Java ---->JNI ------>C/C++
2.2設置Surface
把Java的Surface傳入JNI,由C/C++修改Surface的寬和高
Surface-->>ANativeWindow
2.3、C/C++開始解碼,并把解出來的視頻幀,交由JNI層去顯示到Surface上。
3、代碼實現(xiàn)
圍繞著這三個步驟,我們開始寫代碼。
JNI層要獲取到視頻的寬度和高度,還要拿到每一幀的圖像去渲染,所以就要有方法獲取到視頻的寬度和高度,如果采用直接讀取的方式,有可以會讀取失敗,所以,我采用了回調(diào)的方式。先定義一個接口類(個人這么理解的,對于C/C++不是很通,先這么理解吧)。
class VideoCallBack {
public:
//回調(diào)視頻的寬度和高度
virtual void onSizeChange(int width, int height);
//回調(diào)解碼出來的視頻幀
virtual void onDecoder(AVFrame *avFrame);
};
對應于C/C++層,我這里單獨定義一個解碼的類:FFDecoder
對應于Java層。
class FFDecoder {
public:
FFDecoder();
int setMediaUri(const char *mediaUri);
//在setSurface之后調(diào)用
int setDecoderCallBack(VideoCallBack *videoCallBack);
int startPlayMedia();
private:
int findVideoInfo();
static void *decoderFile(void *);
static void setAVFrame(AVPacket *packet);
};
需要的方法都已定義好了,剩下就是實現(xiàn)了。開始已經(jīng)說過,我們要把Java傳遞過來的路徑轉(zhuǎn)換為FFDecoder能用的const char *mediaUri,然后再傳給FFDecoder,這里用到了JNI數(shù)據(jù)和C/C++的數(shù)據(jù)類型轉(zhuǎn)換,不會的自行百度或者谷歌。不要問我為什么,我也不是很懂。
ffDecoder = new FFDecoder();
const char *mediaUri = env->GetStringUTFChars(mediaPath, NULL);
int flag = ffDecoder->setMediaUri(mediaUri);
LOGE("mediaUri = %s", mediaUri);
LOGE("flag = %d", flag);
return flag;
FFDecoder拿到mediaUri之后,開始解碼讀取文件
av_register_all();
avcodec_register_all();
avformat_network_init();
//前三句是注冊解碼相關(guān)的解碼器,
//FFmpeg里面包含了很多的解碼器,
usleep(2 * 1000);
int input = avformat_open_input(&avFormatContext, mediaPath, NULL, NULL);
if (input < 0) {
input = avformat_open_input(&avFormatContext, mediaPath, NULL, NULL);
}
if (input < 0) {
LOGE(" open input error ,\n input ------->>%d", input);
return -1;
}
//設置最大緩存和最大讀取時長
avFormatContext->probesize = 4096;
avFormatContext->max_analyze_duration = 1500;
int streamInfo = avformat_find_stream_info(avFormatContext, NULL);
if (streamInfo < 0) {
LOGE(" find_stream error ,\n streamInfo ------->>%d", streamInfo);
return -1;
}
// LOGE("streamInfo= %d",streamInfo);
/*
*輸出文件的信息,也就是我們在使用ffmpeg時能夠看到的文件詳細信息,
*第二個參數(shù)指定輸出哪條流的信息,-1代表ffmpeg自己選擇。最后一個參數(shù)用于
*指定dump的是不是輸出文件,我們的dump是輸入文件,因此一定要為0
*/
av_dump_format(avFormatContext, -1, mediaPath, 0);
avPacket = av_packet_alloc();
// avPacket = (AVPacket *)
av_malloc(sizeof(AVPacket));
findVideoResult = findVideoInfo();
if (findVideoResult < 0) {
return -1;
}
return 0;
ps:
這里說明一下為什么我在開始的時候,代碼里面會有一個延時和讀取兩次,因為我在做實際項目中,有一個切換視頻分辨率的功能,在切換的時候,原始的數(shù)據(jù)流斷開了,我這邊需要重新連接,在重新連接的時候,如果我打開的太快,視頻流地址還沒有開啟,所以我就加了一個延時和重新讀取。如果是本地視頻播放,可忽略。
下面是findVideoInfo()的內(nèi)容,最主要的就是獲取videoStreamIndex、video_width和video_height。
int FFDecoder::findVideoInfo() {
//視頻流標志,如果是-1說明沒有找到視頻相關(guān)信息
videoStreamIndex = -1;
for (int i = 0; i < avFormatContext->nb_streams; i++) {
if (avFormatContext->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO
&& videoStreamIndex < 0) {
videoStreamIndex = i;
break;
}
}
if (videoStreamIndex < 0) {
LOGE("Didn't find a video stream ");
return -1;
}
// LOGE("videoStreamIndex --->>%d", videoStreamIndex);
videoStream = avFormatContext->streams[videoStreamIndex];
// Get a pointer to the codec context for the video stream
videoCodecContext = videoStream->codec;
// Find the decoder for the video stream
videoCodec = avcodec_find_decoder(videoCodecContext->codec_id);
if (videoCodec == NULL) {
LOGE("videoAvCodec not found.");
return -1;
}
if (avcodec_open2(videoCodecContext, videoCodec, NULL) < 0) {
LOGE("Could not open videoCodecContext.");
return -1;
}
//視頻幀率
float rate = (float) av_q2d(videoStream->r_frame_rate);
LOGE("rate--------->>%f", rate);
//視頻的寬和高
video_width = videoCodecContext->width;
video_height = videoCodecContext->height;
LOGE("video_width--------->>%d", video_width);
LOGE("video_height--------->>%d", video_height);
if (video_width == 0 || video_height == 0) {
return -1;
}
return 1;
}
然后是設置我們的Surface,并且設置視頻的回調(diào)
mANativeWindow = NULL;
// 獲取native window
mANativeWindow = ANativeWindow_fromSurface(env, surface);
if (mANativeWindow == NULL) {
LOGE("ANativeWindow_fromSurface error");
return;
}
ffDecoder->setDecoderCallBack(new VideoCallBack());
FFDecoder接收到回調(diào)VideoCallBack的指針之后,設置視頻的寬和高并初始化視頻的渲染格式,這里采用的是RGBA。
int FFDecoder::setDecoderCallBack(VideoCallBack *videoCallBack) {
mVideoCallBack = videoCallBack;
mVideoCallBack->onSizeChange(video_width, video_height);
pFrame = av_frame_alloc();
// 用于渲染//
pFrameRGBA = av_frame_alloc();
// Determine required buffer size and allocate buffer
int numBytes = av_image_get_buffer_size(AV_PIX_FMT_YUV420P,
videoCodecContext->width,
videoCodecContext->height,
1);
uint8_t *buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
av_image_fill_arrays(pFrameRGBA->data,pFrameRGBA->linesize,
buffer,AV_PIX_FMT_YUV420P,
videoCodecContext->width,
videoCodecContext->height, 1);
// 由于解碼出來的幀格式不是RGBA的,在渲染之前需要進行格式轉(zhuǎn)換//
sws_ctx = sws_getContext(videoCodecContext->width,//
videoCodecContext->height,//
videoCodecContext->pix_fmt,//
videoCodecContext->width,//
videoCodecContext->height,//
AV_PIX_FMT_YUV420P,//
SWS_FAST_BILINEAR,//
NULL,//
NULL,//
NULL);
}
拿到視頻的寬度和高度之后,進行設置我們的mANativeWindow,并且設置為WINDOW_FORMAT_RGBA_8888。
void VideoCallBack::onSizeChange(int width, int height) {
w_width = width;
w_height = height;
// LOGE("w_width--------->>%d", w_width);
// LOGE("w_height--------->>%d", w_height);
if (w_width == 0 || w_height == 0) {
return;
}
// 設置native window的buffer大小,可自動拉伸//
ANativeWindow_setBuffersGeometry(mANativeWindow, w_width, w_height,//
WINDOW_FORMAT_RGBA_8888);
}
接下來就是開始播放,因為我們要去不斷的讀取視頻里面的AVPacket,并且要從AVPacket里面獲取的原始的AVFrame,所以這些我放在了線程里面去操作。
int FFDecoder::startPlayMedia() {
//開啟文件解碼線程
pthread_create(&decoderThread, NULL, decoderFile, NULL);
}
startPlayMedia只做一件事情,就是開啟解碼的線程,真正要做事的實在
decoderFile這個指針函數(shù)里面
void* FFDecoder::decoderFile(void *) {
while (true){
//usleep(20 * 1000);//中間的延時,如果不加這一句,
//播放本地視頻的時候就如同視頻快進一樣,每一幀圖片一閃而過
int readFrame = av_read_frame(avFormatContext, avPacket);
if (readFrame < 0) {
// LOGE(" readFrame is < 0 ------------->%d", readFrame);
break;
}
int packetStreamIndex = avPacket->stream_index;
if (packetStreamIndex == videoStreamIndex) {
setAVFrame(avPacket);
}
}
}
/**
*
*/
void FFDecoder::setAVFrame(AVPacket *packet) {
int gotFrame = -1;
int line = avcodec_decode_video2(videoCodecContext, pFrame, &gotFrame, packet);
if (line < 0) {
LOGE("line----------->>%d", line);
av_free_packet(packet);
return;
}
if (gotFrame < 0) {
LOGE("gotFrame----------->>%d", gotFrame);
av_free_packet(packet);
return;
}
int errflag = pFrame->decode_error_flags;
if (errflag == 1) {
av_free_packet(packet);
return;
}
// 格式轉(zhuǎn)換//
sws_scale(sws_ctx, (uint8_t const *const *) pFrame->data,//
pFrame->linesize, 0, videoCodecContext->height,//
pFrameRGBA->data, pFrameRGBA->linesize);
//回調(diào)解碼出視頻幀
mVideoCallBack->onDecoder(pFrameRGBA);
av_free_packet(packet);
}
拿到視頻幀之后,剩下的就是如果渲染到Surface上。
void VideoCallBack::onDecoder(AVFrame *avFrame) {
if (w_width == 0 || w_height == 0) {
return;
}
ANativeWindow_lock(mANativeWindow, &windowBuffer, 0);//
if (windowBuffer.stride == 0) {
LOGE("surface 創(chuàng)建失敗");//
return;//
}
// 獲取stride//
uint8_t *dst = (uint8_t *) windowBuffer.bits;//
if (dstStride == 0) {//
dstStride = windowBuffer.stride * 4;//
}
// // LOGE("dstStride------>>>%d", dstStride);
uint8_t *src = avFrame->data[0];
int srcStride = avFrame->linesize[0];
// LOGE("srcStride------>>>%d", srcStride);
// 由于window的stride和幀的stride不同,因此需要逐行復制
int h;//
for (h = 0; h < w_height; h++) {//
memcpy(dst + h * dstStride, src + h * srcStride, srcStride);
}
ANativeWindow_unlockAndPost(mANativeWindow);
}
到這里,核心的代碼已經(jīng)寫完了,剩下的就是去編譯,然后在Java里面去調(diào)用。就可以去播放視頻了。
至于音頻和其他的一些功能,有時間在寫吧。