項目源碼
Android Studio的開發環境已經準備好,接下來開始正式的寫ndk代碼,首先創建一個FFmpeg工具類,添加native方法
import android.view.Surface;
public class FFmpegPlayer {
static {
System.loadLibrary("ffmpeg");
}
/**
* 播放視頻
*/
public native void playVideo(String url,Surface surface);
}
傳入的Surface對象是用於顯示播放界面的,這裏先傳入,後邊再說
然後生成對應的頭文件,之前在eclipse上開發的時候都是通過javah命令生成頭文件後將對應的方法名拷貝到我們的c文件中,這部分的生成方法可以查看之前的博客。現在我們使用的Android Studio3.x可以一鍵生成對應的native方法,將光標選中方法名然後alt+enter選擇create function xxx即可
extern "C"
JNIEXPORT void JNICALL
Java_com_rzm_ffmpegplayer_FFmpegPlayer_playVideo(JNIEnv *env, jobject instance, jstring url_,
jobject surface) {
const char *url = env->GetStringUTFChars(url_, 0);
env->ReleaseStringUTFChars(url_, url);
}
今天來進行的是FFmpeg的第一部分,解封裝
解封裝
一個mp4文件可能是視頻流音頻流字幕流等等多個流的一個結合體,而我們要實現視頻和音頻的播放,就需要將這個結合體拆分開來,單獨的進行視頻播放,音頻播放,實現同步,這是實現播放的一個必要的條件。所以播放的第一步就要進行源文件的解封裝,解封裝涉及到的一些接口如下
av_register_all()
初始化libavformat 並註冊所有的muxers和demuxers以及各種協議,當然,如果你只需要初始化特定的支持組件,那麼單獨調用特定的方法即可,一般直接這樣做一勞永逸
avformat_open_input
打開輸入流(可以是本地流也可以是網絡流,如果是網絡的,那麼需要avformat_network_init()方法支持)並讀取視頻頭信息,視頻頭通常包含一些視頻基本信息,比如說視頻格式,streams的數量等等。注意此時編解碼器未打開,最後必須使用avformat_close_input(&avFormatContext)進行關閉,避免造成泄漏
avformat_network_init()
初始化全局的網絡組件,支持rtfp協議的時候需要打開這個開關,ffmpeg推薦打開這個全局的網絡開關,因爲可以減少單獨對每個單獨回話做設置的開銷
avformat_find_stream_info()
打開流文件之後執行這個方法,讀取媒體文件獲取到流信息,這個方法對於沒有視頻頭的視頻尤其有效(MPEG-2),,此方法執行之後會將讀取到的流信息填充到AVFormatContext中的各個流的軌道上,這樣一來,AVFormatContext->streams[i]中就有信息了.這個方法可以打開部分解碼器並保存在第二個參數AVDictionary中,但是他無法保證打開全部的解碼器,如果存在null那麼也是正常現象,這裏第二個參數我們直接置爲NULL
av_find_best_stream
媒體信息已經獲取到了,接下來需要將視頻流和音頻流區分開來,可以通過av_find_best_stream來獲取流軌道的index,在老版本的ffmpeg上我們是通過遍歷所有的流通過對比codec_type來判斷的,代碼如下
for(int i = 0; i < avFormatContext->nb_streams; i++){
AVStream *avStream = avFormatContext->streams[i];
if (avStream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
//找到視頻index
videoIndex = i;
} else if (avStream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
//找到音頻index
audioIndex = i;
}
}
通過這個方法獲取的方式爲下,驗證一下,你會發現二者結果相同
videoIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);
audioIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);
av_read_frame
返回流的下一幀,這個函數返回文件中存儲的內容。每調用一次,它就將返回一幀數據,這是從文件存儲的內容中切割出來的
av_packet_unref
這個方法會擦除packet空間,不再指向這個packet的緩存空間,另外也會將packet重置爲默認狀態。一幀數據也就是一個ACPacket使用完之後需要回收,否則會造成內存急劇增長,下邊分別是調用這個方法和不調用這個方法的內存變化狀態
完整的代碼如下
/**
* 播放視頻,支持本地和網絡兩種
*/
extern "C"
JNIEXPORT void JNICALL
Java_com_rzm_ffmpegplayer_FFmpegPlayer_playVideo(JNIEnv *env, jobject instance, jstring url_,
jobject surface) {
const char *url = env->GetStringUTFChars(url_, 0);
//初始化解封裝
av_register_all();
//初始化全局網絡組件,可選推薦使用,在使用網絡協議的場景中這是必選的(rtfp http)
avformat_network_init();
AVFormatContext *avFormatContext = NULL;
//指定輸入的格式,如果爲NULL,將自動檢測輸入格式,所以可置爲NULL
//AVInputFormat *fmt = NULL;
//打開輸入文件,可以是本地視頻或者網絡視頻
int result = avformat_open_input(&avFormatContext,url,NULL,NULL);
//打開輸入內容失敗
if(result != 0){
LOGE("avformat_open_input failed!:%s",av_err2str(result));
return;
}
//打開輸入成功
LOGI("avformat_open_input success!:%s",av_err2str(result));
//讀取媒體文件的分組以獲得流信息。這個對於沒有標題的文件格式(如MPEG)很有用。這個函數還計算實際的幀率在
//MPEG-2重複的情況下幀模式。
result = avformat_find_stream_info(avFormatContext,NULL);
if (result < 0){
LOGE("avformat_find_stream_info failed: %s",av_err2str(result));
}
//獲取到了輸入文件信息,打印一下視頻時長和nb_streams
LOGI("duration = %lld nb_streams=%d",avFormatContext->duration,avFormatContext->nb_streams);
//分離音視頻,獲取音視頻在源文件中的streams index
int videoIndex = 0;
int audioIndex = 1;
int fps = 0;
for(int i = 0; i < avFormatContext->nb_streams; i++){
AVStream *avStream = avFormatContext->streams[i];
if (avStream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
//找到視頻index
videoIndex = i;
LOGI("video index = %d",videoIndex);
//FPS是圖像領域中的定義,是指畫面每秒傳輸幀數
fps = r2d(avStream->avg_frame_rate);
LOGI("video info ---- fps = %d fps den= %d fps num= %d width=%d height=%d code id=%d format=%d",
fps,
avStream->avg_frame_rate.den,
avStream->avg_frame_rate.num,
avStream->codecpar->width,
avStream->codecpar->height,
avStream->codecpar->codec_id,
avStream->codecpar->format
);
} else if (avStream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
//找到音頻index
audioIndex = i;
LOGI("audio index = %d sampe_rate=%d channels=%d sample_format=%d",
audioIndex,
avStream->codecpar->sample_rate,
avStream->codecpar->channels,
avStream->codecpar->format
);
}
}
//上邊通過遍歷streams音視頻的index,還可以通過提供的接口獲取
videoIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);
audioIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);
LOGI("av_find_best_stream videoIndex=%d audioIndex=%d",videoIndex,audioIndex);
//讀取幀數據
//Allocate an AVPacket and set its fields to default values
//存儲壓縮數據,對於視頻,它通常應該包含一個壓縮幀。對於音頻它可能包含幾個壓縮幀
AVPacket *avPacket = av_packet_alloc();
for (;;) {
//Return the next frame of a stream.
int read_result = av_read_frame(avFormatContext,avPacket);
if(read_result != 0){
//讀取到結尾處,從20秒位置繼續開始播放
LOGI("讀取到結尾處 %s",av_err2str(read_result));
//跳轉到指定的position播放,最後一個參數表示
//int pos = 200000 * r2d(avFormatContext->streams[videoIndex]->time_base);
//av_seek_frame(avFormatContext,videoIndex,pos,AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME );
//LOGI("avFormatContext->streams[videoIndex]->time_base= %d",avFormatContext->streams[videoIndex]->time_base);
//continue;
break;
}
LOGW("stream = %d size =%d pts=%lld flag=%d pos = %d",
avPacket->stream_index,avPacket->size,avPacket->pts,avPacket->flags,avPacket->pos
);
//packet使用完成之後執行,否則內存會急劇增長
//不再引用這個packet指向的空間,並且將packet置爲default狀態
av_packet_unref(avPacket);
}
//關閉上下文
avformat_close_input(&avFormatContext);
env->ReleaseStringUTFChars(url_, url);
}