之前已經把FFmpeg集成到項目里面了酱虎,剩下的就是做開發(fā)了特幔,做過安卓視頻播放的都應該知道在播放的時候都有用到SurfaceView缅糟,這里我們也采用這種方式囤热。
一驾窟、定義Java層的調用接口
- 我們需要知道播放視頻的網絡地址或者是本地路徑废累,并且希望這個地址是可以修改的十气,所以我們需要有一個參數去接收這個地址丧蘸。
- 和系統(tǒng)一樣窟勃,我們也需要傳遞一個Surface,在Jni中沒有Surface這個類型祖乳,所以要用Object(JNI中除了基本的數據類型,其他的都用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轉換成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層要獲取到視頻的寬度和高度妇多,還要拿到每一幀的圖像去渲染伤哺,所以就要有方法獲取到視頻的寬度和高度,如果采用直接讀取的方式者祖,有可以會讀取失敗立莉,所以,我采用了回調的方式咸包。先定義一個接口類(個人這么理解的,對于C/C++不是很通桃序,先這么理解吧)杖虾。
class VideoCallBack {
public:
//回調視頻的寬度和高度
virtual void onSizeChange(int width, int height);
//回調解碼出來的視頻幀
virtual void onDecoder(AVFrame *avFrame);
};
對應于C/C++層烂瘫,我這里單獨定義一個解碼的類:FFDecoder
對應于Java層。
class FFDecoder {
public:
FFDecoder();
int setMediaUri(const char *mediaUri);
//在setSurface之后調用
int setDecoderCallBack(VideoCallBack *videoCallBack);
int startPlayMedia();
private:
int findVideoInfo();
static void *decoderFile(void *);
static void setAVFrame(AVPacket *packet);
};
需要的方法都已定義好了奇适,剩下就是實現(xiàn)了坟比。開始已經說過,我們要把Java傳遞過來的路徑轉換為FFDecoder能用的const char *mediaUri嚷往,然后再傳給FFDecoder葛账,這里用到了JNI數據和C/C++的數據類型轉換,不會的自行百度或者谷歌皮仁。不要問我為什么籍琳,我也不是很懂菲宴。
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();
//前三句是注冊解碼相關的解碼器趋急,
//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時能夠看到的文件詳細信息呜达,
*第二個參數指定輸出哪條流的信息谣蠢,-1代表ffmpeg自己選擇。最后一個參數用于
*指定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:
這里說明一下為什么我在開始的時候,代碼里面會有一個延時和讀取兩次霜威,因為我在做實際項目中谈喳,有一個切換視頻分辨率的功能,在切換的時候戈泼,原始的數據流斷開了叁执,我這邊需要重新連接,在重新連接的時候矮冬,如果我打開的太快谈宛,視頻流地址還沒有開啟,所以我就加了一個延時和重新讀取胎署。如果是本地視頻播放吆录,可忽略。
下面是findVideoInfo()的內容琼牧,最主要的就是獲取videoStreamIndex恢筝、video_width和video_height。
int FFDecoder::findVideoInfo() {
//視頻流標志巨坊,如果是-1說明沒有找到視頻相關信息
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,并且設置視頻的回調
mANativeWindow = NULL;
// 獲取native window
mANativeWindow = ANativeWindow_fromSurface(env, surface);
if (mANativeWindow == NULL) {
LOGE("ANativeWindow_fromSurface error");
return;
}
ffDecoder->setDecoderCallBack(new VideoCallBack());
FFDecoder接收到回調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的,在渲染之前需要進行格式轉換//
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這個指針函數里面
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;
}
// 格式轉換//
sws_scale(sws_ctx, (uint8_t const *const *) pFrame->data,//
pFrame->linesize, 0, videoCodecContext->height,//
pFrameRGBA->data, pFrameRGBA->linesize);
//回調解碼出視頻幀
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);
}
到這里悬槽,核心的代碼已經寫完了贝椿,剩下的就是去編譯,然后在Java里面去調用陷谱。就可以去播放視頻了烙博。
至于音頻和其他的一些功能,有時間在寫吧烟逊。