目錄

相關(guān)文章
效果展示

實(shí)現(xiàn)流程

實(shí)現(xiàn)步驟
1.布局添加SurfaceView用于顯示視頻
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<SurfaceView
android:id="@+id/sfv_player"
android:layout_width="match_parent"
android:layout_height="300dp"/>
<Button
android:id="@+id/bt_play"
android:text="播放"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
2.將Surface傳給NDK層
public class MainActivity extends AppCompatActivity{
private SurfaceView surfaceView;
private Button button;
static {
System.loadLibrary("native-lib");
}
private SurfaceHolder mHolder;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
surfaceView = findViewById(R.id.sfv_player);
button = findViewById(R.id.bt_play);
button.setOnClickListener(v->{
//找到SD卡中的視頻文件
File video = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),"001.mp4");
//子線程進(jìn)行視頻渲染
new Thread(new Runnable() {
@Override
public void run() {
native_start_play(video.getAbsolutePath(),mHolder.getSurface());
}
}).start();
});
initSurfaceHolder();
}
private void initSurfaceHolder() {
mHolder = surfaceView.getHolder();
mHolder.setFormat(PixelFormat.RGBA_8888);
}
/**
* 調(diào)用NDK的視頻渲染
* @param path 播放的視頻的路徑
* @param surface 要渲染的surface
* @return
*/
public native void native_start_play(String path, Surface surface);
}
3.NDK層進(jìn)行視頻流渲染
這里的邏輯是根據(jù)實(shí)現(xiàn)流程來(lái)的,每一步都加入了注釋
#include <jni.h>
#include <string>
#include <android/native_window_jni.h>
#include <zconf.h>
//混合C代碼編譯
extern "C"{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
}
extern "C"
JNIEXPORT void JNICALL
Java_com_itfitness_ffmpegdemo_MainActivity_native_1start_1play(JNIEnv *env, jobject thiz,
jstring path, jobject surface) {
//獲取用于繪制的NativeWindow
ANativeWindow *a_native_window = ANativeWindow_fromSurface(env,surface);
//轉(zhuǎn)換視頻路徑字符串為C中可用的
const char *video_path = env->GetStringUTFChars(path,0);
//網(wǎng)絡(luò)模塊初始化(可以播放Url)
avformat_network_init();
//獲取用于獲取視頻文件中各種流(視頻流、音頻流、字幕流等)的上下文:AVFormatContext
AVFormatContext *av_format_context = avformat_alloc_context();
//配置信息
AVDictionary *options = NULL;
av_dict_set(&options,"timeout","3000000",0);
//打開(kāi)視頻文件
//第一個(gè)參數(shù):AVFormatContext的二級(jí)指針
//第二個(gè)參數(shù):視頻路徑
//第三個(gè)參數(shù):非NULL的話就是設(shè)置輸入格式,NULL就是自動(dòng)
//第四個(gè)參數(shù):配置項(xiàng)
//返回值是是否打開(kāi)成功,0是成功其他為失敗
int open_result = avformat_open_input(&av_format_context, video_path, NULL, &options);
//如果打開(kāi)失敗就返回
if(open_result){
return;
}
//讓FFmpeg將流解析出來(lái),并找到視頻流對(duì)應(yīng)的索引
avformat_find_stream_info(av_format_context, NULL);
int video_stream_index = 0;
for(int i = 0; i < av_format_context->nb_streams ; i++){
//如果當(dāng)前流是視頻流的話保存索引
if(av_format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
video_stream_index = i;
break;
}
}
//獲取視頻流的解碼參數(shù)(寬高等信息)
AVCodecParameters * av_codec_parameters = av_format_context->streams[video_stream_index]->codecpar;
//獲取視頻流的解碼器
AVCodec *av_codec = avcodec_find_decoder(av_codec_parameters->codec_id);
//獲取解碼上下文
AVCodecContext * av_codec_context = avcodec_alloc_context3(av_codec);
//將解碼器參數(shù)復(fù)制到解碼上下文(因?yàn)榻獯a上下文目前還沒(méi)有解碼器參數(shù))
avcodec_parameters_to_context(av_codec_context,av_codec_parameters);
//進(jìn)行解碼
avcodec_open2(av_codec_context,av_codec,NULL);
//因?yàn)閅UV數(shù)據(jù)被封裝在了AVPacket中,因此我們需要用AVPacket去獲取數(shù)據(jù)
AVPacket *av_packet = av_packet_alloc();
//獲取轉(zhuǎn)換上下文(把解碼后的YUV數(shù)據(jù)轉(zhuǎn)換為RGB數(shù)據(jù)才能在屏幕上顯示)
SwsContext *sws_context = sws_getContext(av_codec_context->width,av_codec_context->height,av_codec_context->pix_fmt,
av_codec_context->width,av_codec_context->height,AV_PIX_FMT_RGBA,SWS_BILINEAR,
0,0,0);
//設(shè)置NativeWindow繪制的緩沖區(qū)
ANativeWindow_setBuffersGeometry(a_native_window,av_codec_context->width,av_codec_context->height,
WINDOW_FORMAT_RGBA_8888);
//繪制時(shí),用于接收的緩沖區(qū)
ANativeWindow_Buffer a_native_window_buffer;
//計(jì)算出轉(zhuǎn)換為RGB所需要的容器的大小
//接收的容器
uint8_t *dst_data[4];
//每一行的首地址(R、G、B、A四行)
int dst_line_size[4];
//進(jìn)行計(jì)算
av_image_alloc(dst_data,dst_line_size,av_codec_context->width,av_codec_context->height,
AV_PIX_FMT_RGBA,1);
//從視頻流中讀數(shù)據(jù)包,返回值小于0的時(shí)候表示讀取完畢
while (av_read_frame(av_format_context,av_packet) >= 0){
//將取出的數(shù)據(jù)發(fā)送出來(lái)
avcodec_send_packet(av_codec_context,av_packet);
//接收發(fā)送出來(lái)的數(shù)據(jù)
AVFrame *av_frame = av_frame_alloc();
int av_receive_result = avcodec_receive_frame(av_codec_context,av_frame);
//如果讀取失敗就重新讀
if(av_receive_result == AVERROR(EAGAIN)){
continue;
} else if(av_receive_result < 0){
//如果到末尾了就結(jié)束循環(huán)讀取
break;
}
//將取出的數(shù)據(jù)放到之前定義的RGB目標(biāo)容器中
sws_scale(sws_context,av_frame->data,av_frame->linesize,0,av_frame->height,
dst_data,dst_line_size);
//加鎖然后進(jìn)行渲染
ANativeWindow_lock(a_native_window,&a_native_window_buffer,0);
uint8_t *first_window = static_cast<uint8_t *>(a_native_window_buffer.bits);
uint8_t *src_data = dst_data[0];
//拿到每行有多少個(gè)RGBA字節(jié)
int dst_stride = a_native_window_buffer.stride * 4;
int src_line_size = dst_line_size[0];
//循環(huán)遍歷所得到的緩沖區(qū)數(shù)據(jù)
for(int i = 0; i < a_native_window_buffer.height;i++){
//內(nèi)存拷貝進(jìn)行渲染
memcpy(first_window+i*dst_stride,src_data+i*src_line_size,dst_stride);
}
//繪制完解鎖
ANativeWindow_unlockAndPost(a_native_window);
//40000微秒之后解析下一幀(這個(gè)是根據(jù)視頻的幀率來(lái)設(shè)置的,我這播放的視頻幀率是25幀/秒)
usleep(1000 * 40);
//釋放資源
av_frame_free(&av_frame);
av_free_packet(av_packet);
}
env->ReleaseStringUTFChars(path,video_path);
}
注意
●視頻沒(méi)聲音
這里只是渲染了視頻的畫(huà)面數(shù)據(jù),并沒(méi)有進(jìn)行聲音的處理,因此沒(méi)有聲音是正常的
●每一幀畫(huà)面的延遲時(shí)間
每一幀畫(huà)面渲染的延遲時(shí)間是根據(jù)視頻的信息來(lái)設(shè)置的,我案例中的視頻是25幀/秒,換算出來(lái)是一幀40000微秒,否則會(huì)出現(xiàn)視頻畫(huà)面播放過(guò)快或過(guò)慢的問(wèn)題

優(yōu)化補(bǔ)充
●視頻幀的延遲優(yōu)化
之前的延遲時(shí)間是固定寫(xiě)死的這樣來(lái)說(shuō)不大好,因?yàn)槊總€(gè)視頻的幀率可能不一樣,這樣就不能適配所有的視頻,因此這里我們優(yōu)化下,使用FFmpeg的API獲取到視頻的幀率然后計(jì)算出每幀的延遲時(shí)間,具體如下,我們通過(guò)編解碼上下文獲取到視頻流里的avg_frame_rate其中frame_rate.num其實(shí)就是視頻的幀率(因?yàn)?strong>frame_rate.den一般是1),不過(guò)我們還是用兩個(gè)數(shù)值來(lái)計(jì)算下,然后我們?cè)偻ㄟ^(guò)這個(gè)幀率來(lái)計(jì)算出每幀的延遲時(shí)間即可
//計(jì)算出視頻的幀率
AVRational frame_rate = avFormatContext->streams[video_stream_id]->avg_frame_rate;
double fps = frame_rate.num / frame_rate.den;
//計(jì)算出延時(shí)時(shí)間(單位:微秒)
double delayTime = 1.0f / fps * 1000000;
最后延時(shí)哪里也需要改下
