前言
本篇文章屬于 Android NDK 模塊,需要讀者有一點(diǎn) NDK 相關(guān)的基礎(chǔ)和 C/C++ 基礎(chǔ)棠笑,不然其中的語法會(huì)有點(diǎn)晦澀難懂。本篇文章共分為以下五個(gè)專題,通過這五個(gè)專題的學(xué)習(xí)最終帶大家制作一款屬于自己的直播流播放器硫椰。
- 直播流信息獲取
- 視頻解碼與原生繪制
- 音頻解碼與 OpenSL
- 音視頻同步
- 音視頻停止與釋放
在學(xué)習(xí)第一個(gè)專題之前我們先掌握一些基礎(chǔ)知識(shí)。我們知道播放在手機(jī)上的視頻圖像是由 RGB 三原色組成的萨蚕,視頻的話是各種圖片的集合靶草,由于 RGB 數(shù)據(jù)量太大我們需要進(jìn)行壓縮減少數(shù)據(jù)大小節(jié)省帶寬和磁盤空間。為什么可以壓縮呢岳遥,可以從以下幾個(gè)方面進(jìn)行考慮奕翔。
去除冗余信息
- 空間冗余:圖像相鄰像素之間有較強(qiáng)的相關(guān)性
- 時(shí)間冗余:視頻序列的相鄰圖像之間內(nèi)容相似
- ?編碼冗余:不同像素值出現(xiàn)的概率不同?- 視覺冗余:人的視覺系統(tǒng)對(duì)某些細(xì)節(jié)不敏感
- ?知識(shí)冗余:規(guī)律性的結(jié)構(gòu)可由先驗(yàn)知識(shí)和背景知識(shí)得到
我們需要做的是獲得壓縮數(shù)據(jù)解壓縮展示在手機(jī)上。那具體的流程是什么樣的呢浩蓉?比如說我們獲得了一個(gè) MP4 文件派继,如何解壓縮成為可以展示的 RGB 圖像呢?
我們可以借助 FFmpeg 進(jìn)行解封裝解碼的工作捻艳,F(xiàn)Fmpeg 不僅內(nèi)部實(shí)現(xiàn)了編解碼算法還可以集成其他的編解碼框架互艾,目前抖音斗魚等各種直播軟件都使用了 FFmpeg 所以說還是很強(qiáng)大的。
Android 做視頻只能通過 FFmpeg 嗎讯泣?FFmpeg 是通過軟編解碼通過代碼進(jìn)行編解碼纫普,還有一個(gè)硬編解碼,Android 中的 Media Codec 使用的就是硬編解碼好渠,由于兼容性比較差如果沒有廠商的硬件支持基本上是兼容不了昨稼。OK,下面首先介紹一下我們的工程結(jié)構(gòu)拳锚。
我的 build 文件假栓,配置 CPU 兼容 和 CMakeLists:
externalNativeBuild {
cmake {
cppFlags ""
abiFilters 'armeabi-v7a'
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
externalNativeBuild {
cmake {
path "CMakeLists.txt"
}
}
我的 CMakeLists 文件
cmake_minimum_required(VERSION 3.4.1)
# 創(chuàng)建一個(gè)變量 source_file 它的值就是src/main/cpp/ 所有的.cpp文件
file(GLOB source_file src/main/cpp/*.cpp)
add_library(
native-lib
SHARED
${source_file} )
include_directories(src/main/cpp/include)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -L${CMAKE_SOURCE_DIR}/src/main/cpp/libs/${ANDROID_ABI}")
#avfilter avformat avcodec avutil swresample swscale
target_link_libraries( native-lib
avformat avcodec avfilter avutil swresample swscale
log z)
Java 層我們新建兩個(gè)文件,MainActivity 播放器的主界面霍掺,MyPlayer 播放器的管理類匾荆。
cpp 中的 libs 存放的是我們編譯出來的 FFmpeg 的 .a 庫,JavaCallHelper 類實(shí)現(xiàn)了 C++ 反射調(diào)用 Java 代碼杆烁,MyFFmpeg 中編寫獲取直播流解碼的代碼牙丽,獲取到流后分為視頻流和音頻流,分別用 VideoChannel 和 AudioChannel 類進(jìn)行處理兔魂。為了方便我編寫了一個(gè) util.h 文件來定義一些宏函數(shù)烤芦,由于直播涉及到線程我編寫了一個(gè)線程安全的類 safe_queue。OK析校,整體項(xiàng)目結(jié)構(gòu)就是這樣构罗,下面開始進(jìn)行代碼的講解铜涉。
直播流信息獲取
public class MainActivity extends AppCompatActivity {
private MyPlayer myPlayer;
/**
* 1,RTMP協(xié)議直播源
* 香港衛(wèi)視:rtmp://live.hkstv.hk.lxdns.com/live/hks
*
* 2遂唧,RTSP協(xié)議直播源
* 珠海過澳門大廳攝像頭監(jiān)控:rtsp://218.204.223.237:554/live/1/66251FC11353191F/e7ooqwcfbqjoo80j.sdp
* 大熊兔(點(diǎn)播):rtsp://184.72.239.149/vod/mp4://BigBuckBunny_175k.mov
*
* 3芙代,HTTP協(xié)議直播源
* 香港衛(wèi)視:http://live.hkstv.hk.lxdns.com/live/hks/playlist.m3u8
* CCTV1高清:http://ivi.bupt.edu.cn/hls/cctv1hd.m3u8
* CCTV3高清:http://ivi.bupt.edu.cn/hls/cctv3hd.m3u8
* CCTV5高清:http://ivi.bupt.edu.cn/hls/cctv5hd.m3u8
* CCTV5+高清:http://ivi.bupt.edu.cn/hls/cctv5phd.m3u8
* CCTV6高清:http://ivi.bupt.edu.cn/hls/cctv6hd.m3u8
* 蘋果提供的測試源(點(diǎn)播):http://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/gear2/prog_index.m3u8
*
* @param savedInstanceState
*/
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
SurfaceView surfaceView = findViewById(R.id.surfaceView);
myPlayer = new MyPlayer();
myPlayer.setSurfaceView(surfaceView);
myPlayer.setDataSource("rtmp://live.hkstv.hk.lxdns.com/live/hks");
myPlayer.setOnPrepareListener(new MyPlayer.OnPrepareListener() {
@Override
public void onPrepare() {
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "可以開始播放了", 0).show();
}
});
}
});
}
public void start(View view) {
myPlayer.prepare();
}}
主界面主要做的工作就是用 SurfaceView 來播放音視頻流,實(shí)例化 MyPlayer 將 surfaceView 傳遞給 MyPlayer 設(shè)置播放地址盖彭,實(shí)現(xiàn)一個(gè)準(zhǔn)備播放的監(jiān)聽链蕊,一個(gè)開始播放的方法。
/**
* 提供java 進(jìn)行播放 停止 等函數(shù)
*/
public class MyPlayer implements SurfaceHolder.Callback {
static {
System.loadLibrary("native-lib");
}
private String dataSource;
private SurfaceHolder holder;
private OnPrepareListener listener;
/**
* 讓使用 設(shè)置播放的文件 或者 直播地址
*/
public void setDataSource(String dataSource) {
this.dataSource = dataSource;
}
/**
* 設(shè)置播放顯示的畫布
*
* @param surfaceView
*/
public void setSurfaceView(SurfaceView surfaceView) {
holder = surfaceView.getHolder();
holder.addCallback(this);
}
public void onError(int errorCode){
System.out.println("Java接到回調(diào):"+errorCode);
}
public void onPrepare(){
if (null != listener){
listener.onPrepare();
}
}
public void setOnPrepareListener(OnPrepareListener listener){
this.listener = listener;
}
public interface OnPrepareListener{
void onPrepare();
}
/**
* 準(zhǔn)備好 要播放的視頻
*/
public void prepare() {
native_prepare(dataSource);
}
/**
* 開始播放
*/
public void start() {
}
/**
* 停止播放
*/
public void stop() {
}
public void release() {
holder.removeCallback(this);
}
/**
* 畫布創(chuàng)建好了
*
* @param holder
*/
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
/**
* 畫布發(fā)生了變化(橫豎屏切換谬泌、按了home都會(huì)回調(diào)這個(gè)函數(shù))
*
* @param holder
* @param format
* @param width
* @param height
*/
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
}
/**
* 銷毀畫布 (按了home/退出應(yīng)用/)
*
* @param holder
*/
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
native void native_prepare(String dataSource);}
MyPlayer 類實(shí)現(xiàn)了一個(gè) SurfaceHolder.Callback 接口滔韵,我們看下源碼:
public interface Callback {
/**
* This is called immediately after the surface is first created.
* Implementations of this should start up whatever rendering code
* they desire. Note that only one thread can ever draw into
* a {@link Surface}, so you should not draw into the Surface here
* if your normal rendering will be in another thread.
*
* @param holder The SurfaceHolder whose surface is being created.
*/
public void surfaceCreated(SurfaceHolder holder);
/**
* This is called immediately after any structural changes (format or
* size) have been made to the surface. You should at this point update
* the imagery in the surface. This method is always called at least
* once, after {@link #surfaceCreated}.
*
* @param holder The SurfaceHolder whose surface has changed.
* @param format The new PixelFormat of the surface.
* @param width The new width of the surface.
* @param height The new height of the surface.
*/
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height);
/**
* This is called immediately before a surface is being destroyed. After
* returning from this call, you should no longer try to access this
* surface. If you have a rendering thread that directly accesses
* the surface, you must ensure that thread is no longer touching the
* Surface before returning from this function.
*
* @param holder The SurfaceHolder whose surface is being destroyed.
*/
public void surfaceDestroyed(SurfaceHolder holder);
}
可以看到這個(gè)接口有三個(gè)方法要我們實(shí)現(xiàn),控制了畫布創(chuàng)建掌实、畫布改變陪蜻、畫布銷毀。在 setSurfaceView 中 getHolder 和 addCallback贱鼻,在 release() 方法中調(diào)用 holder.removeCallback 釋放掉 Callback宴卖,防止內(nèi)存泄漏。由于視頻的編解碼操作是在 native 方法中邻悬,所以定義一個(gè) native_prepare 方法把 dataSource 傳進(jìn)去烙肺。下面開始 native 方法的編寫富拗。在 Myplayer 類中編寫 native void native_prepare(String dataSource);
通過快捷鍵 Alt+Enter 可以在 native-lib 中快速生成相對(duì)應(yīng)的 native 方法。
MyFFmpeg *ffmpeg = 0;
extern "C"
JNIEXPORT void JNICALL
Java_com_my_player_MyPlayer_native_1prepare(JNIEnv *env, jobject instance,
jstring dataSource_) {
const char *dataSource = env->GetStringUTFChars(dataSource_, 0);
//創(chuàng)建播放器
JavaCallHelper *helper = new JavaCallHelper(javaVm, env, instance);
ffmpeg = new MyFFmpeg(helper, dataSource);
ffmpeg->prepare();
env->ReleaseStringUTFChars(dataSource_, dataSource);
}
第一個(gè)參數(shù) JNIEnv 類型實(shí)際上代表了 Java 環(huán)境,通過這個(gè) JNIEnv* 指針购笆,就可以對(duì) Java 端的代碼進(jìn)行操作煞肾。例如刀疙,創(chuàng)建 Java 類中的對(duì)象舔痕,調(diào)用 Java 對(duì)象的方法,獲取 Java 對(duì)象中的屬性等等镀首。JNIEnv 的指針會(huì)被 JNI 傳入到本地方法的實(shí)現(xiàn)函數(shù)中來對(duì) Java 端的代碼進(jìn)行操作坟漱。JNIEnv 類中有很多函數(shù)可以用:
-
NewObject
:創(chuàng)建 Java 類中的對(duì)象 -
NewString
:創(chuàng)建 Java 類中的 String 對(duì)象 -
New<Type>Array
:創(chuàng)建類型為 Type 的數(shù)組對(duì)象 -
Get<Type>Field
:獲取類型為 Type 的字段 -
Set<Type>Field
:設(shè)置類型為 Type 的字段的值 -
GetStatic<Type>Field
:獲取類型為 Type 的 static 的字段 -
SetStatic<Type>Field
:設(shè)置類型為 Type 的 static 的字段的值 -
Call<Type>Method
:調(diào)用返回類型為 Type 的方法 -
CallStatic<Type>Method
:調(diào)用返回值類型為 Type 的 static 方法
等許多的函數(shù),具體的可以查看 jni.h 文件中的函數(shù)名稱更哄。
參數(shù):jobject instance
- 如果 native 方法不是 static 的話芋齿,這個(gè) instance 就代表這個(gè) native 方法的類實(shí)例。
- 如果 native 方法是 static 的話成翩,這個(gè) instance 就代表這個(gè) native 方法的類的 class 對(duì)象實(shí)例(static 方法不需要類實(shí)例的觅捆,所以就代表這個(gè)類的 class 對(duì)象)。
將所有的操作封裝在 Myffmpeg 中捕传,在 native-lib 中進(jìn)行調(diào)用惠拭。
MyFFmpeg::MyFFmpeg(JavaCallHelper *callHelper,const char *dataSource) {
this->callHelper = callHelper;
//防止 dataSource參數(shù) 指向的內(nèi)存被釋放
this->dataSource = new char[strlen(dataSource)];
//錯(cuò)誤寫法 this->dataSource = const_cast<char * >(dataSource);
strcpy(this->dataSource,dataSource); }
MyFFmpeg::~MyFFmpeg() {
//釋放
DELETE(dataSource);
DELETE(callHelper);}
構(gòu)造方法中的錯(cuò)誤寫法是因?yàn)?MyFFmpeg 的成員直接指向參數(shù) dataSource 的話有可能這個(gè) dataSource 在其他地方被釋放了導(dǎo)致 MyFFmpeg 中的 dataSource 變成一個(gè)懸空指針。strcpy 指的是字符串拷貝庸论。在析構(gòu)方法中釋放 dataSource 和 callHelper职辅。到此我們的 MyFFmpeg 已經(jīng)拿到了播放的地址,接下來要對(duì)這個(gè)地址進(jìn)行解析聂示。
解碼流程
播放直播需要連接網(wǎng)絡(luò)所以我們要加上網(wǎng)絡(luò)權(quán)限域携,而且聯(lián)網(wǎng)的操作肯定不能在主線程進(jìn)行,所以我們要開辟一個(gè)子線程在子線程中操作鱼喉。
void* task_prepare(void *args){
MyFFmpeg *ffmpeg = static_cast<MyFFmpeg *>(args);
ffmpeg->_prepare();
return 0;}
void MyFFmpeg::prepare(){
//創(chuàng)建一個(gè)線程
pthread_create(&pid,0,task_prepare,this);
}
導(dǎo)入頭文件 pthread.h秀鞭、libavformat/avformat.h
通過查看pthread.h 頭文件int pthread_create(pthread_t* __pthread_ptr, pthread_attr_t const* __attr, void* (*__start_routine)(void*), void*);
- 第一個(gè)參數(shù)為指向線程標(biāo)識(shí)符的指針
- 第二個(gè)參數(shù)用來設(shè)置線程屬性
- 第三個(gè)參數(shù)是線程運(yùn)行函數(shù)的起始地址
- 最后一個(gè)參數(shù)是運(yùn)行函數(shù)的參數(shù)
void MyFFmpeg::_prepare(){
// 初始化網(wǎng)絡(luò) 讓ffmpeg能夠使用網(wǎng)絡(luò)
avformat_network_init();
//1、打開媒體地址(文件地址扛禽、直播地址)
// AVFormatContext 包含了 視頻的 信息(寬锋边、高等)
formatContext = 0;
//文件路徑不對(duì) 手機(jī)沒網(wǎng)
int ret = avformat_open_input(&formatContext,dataSource,0,0);
//ret不為0表示 打開媒體失敗
if(ret != 0){
LOGE("打開媒體失敗:%s",av_err2str(ret));
callHelper->onError(THREAD_CHILD,FFMPEG_CAN_NOT_OPEN_URL);
return;
}
//2、查找媒體中的 音視頻流 (給 contxt里的 streams等成員賦)
ret = avformat_find_stream_info(formatContext,0);
// 小于0 則失敗
if (ret < 0){
LOGE("查找流失敗:%s",av_err2str(ret));
callHelper->onError(THREAD_CHILD,FFMPEG_CAN_NOT_FIND_STREAMS);
return;
}
//nb_streams :幾個(gè)流(幾段視頻/音頻)
for (int i = 0; i < formatContext->nb_streams; ++i) {
//可能代表是一個(gè)視頻 也可能代表是一個(gè)音頻
AVStream *stream = formatContext->streams[i];
//包含了 解碼 這段流 的各種參數(shù)信息(寬编曼、高豆巨、碼率、幀率)
AVCodecParameters *codecpar = stream->codecpar;
//無論視頻還是音頻都需要干的一些事情(獲得解碼器)
// 1掐场、通過 當(dāng)前流 使用的 編碼方式往扔,查找解碼器
AVCodec *dec = avcodec_find_decoder(codecpar->codec_id);
if(dec == NULL){
LOGE("查找解碼器失敗:%s",av_err2str(ret));
callHelper->onError(THREAD_CHILD,FFMPEG_FIND_DECODER_FAIL);
return;
}
//2、獲得解碼器上下文
AVCodecContext *context = avcodec_alloc_context3(dec);
if(context == NULL){
LOGE("創(chuàng)建解碼上下文失敗:%s",av_err2str(ret));
callHelper->onError(THREAD_CHILD,FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
return;
}
ret = avcodec_parameters_to_context(context,codecpar);
//失敗
if(ret < 0){
LOGE("設(shè)置解碼上下文參數(shù)失敗:%s",av_err2str(ret));
callHelper->onError(THREAD_CHILD,FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);
return;
}
// 4熊户、打開解碼器
ret = avcodec_open2(context,dec,0);
if (ret != 0){
LOGE("打開解碼器失敗:%s",av_err2str(ret));
callHelper->onError(THREAD_CHILD,FFMPEG_OPEN_DECODER_FAIL);
return;
}
//音頻
if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
audioChannel = new AudioChannel;
} else if(codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
videoChannel = new VideoChannel;
}
//沒有音視頻
if(!audioChannel && !videoChannel){
LOGE("沒有音視頻");
callHelper->onError(THREAD_CHILD,FFMPEG_NOMEDIA);
return;
}
// 準(zhǔn)備完了 通知java 你隨時(shí)可以開始播放
callHelper->onPrepare(THREAD_CHILD);
以上代碼包含了獲取音視頻信息(寬高等)萍膛、查找解碼器、獲得解碼器上下文嚷堡、設(shè)置上下文參數(shù)蝗罗、打開解碼器,下面就幾個(gè)重要的方法進(jìn)行講解蝌戒。
avformat_open_input 打開頭文件可以看到 int avformat_open_input(AVFormatContext **ps, const char *url, AVInputFormat *fmt, AVDictionary **options);
绿饵,里面?zhèn)鬟f了 AVFormatContext 點(diǎn)進(jìn)去看,里面包含和音視頻的信息(寬高等)瓶颠,第二個(gè)參數(shù)傳遞了地址拟赊,后面兩個(gè)參數(shù)分別代表文件容器格式、最大延時(shí)粹淋,超時(shí)時(shí)間吸祟,以及支持的協(xié)議的白名單等。
可以看到 avformat_open_input 方法是有返回值的桃移,返回 0 是成功屋匕,不為 0 失敗〗杞埽可以通過我之前編寫的 JavaCallHelper 返回給 Java 進(jìn)行處理过吻,在這里我們需要注意的是,我們是在子線程中反射調(diào)用 Java,而 JNIEnv 不能跨線程調(diào)用這里就涉及到跨線程問題纤虽。這里我們需要一個(gè) JavaVM 來獲得對(duì)應(yīng)線程的 JNIEnv乳绕。
class JavaCallHelper {
public:
JavaCallHelper(JavaVM *vm,JNIEnv* env,jobject instace);
~JavaCallHelper();
//回調(diào)java
void onError(int thread,int errorCode);
void onPrepare(int thread);
private:
JavaVM *vm;
JNIEnv *env;
jobject instance;
jmethodID onErrorId;
jmethodID onPrepareId;
下面介紹下我的 JavaCallHelper 類,其中包含了所有的回調(diào) Java 的方法逼纸。
JavaCallHelper::JavaCallHelper(JavaVM *vm, JNIEnv *env, jobject instace) {
this->vm = vm;
//如果在主線程 回調(diào)
this->env = env;
// 一旦涉及到j(luò)object 跨方法 跨線程 就需要?jiǎng)?chuàng)建全局引用
this->instance = env->NewGlobalRef(instace);
jclass clazz = env->GetObjectClass(instace);
onErrorId = env->GetMethodID(clazz,"onError","(I)V");
onPrepareId = env->GetMethodID(clazz,"onPrepare","()V");}
JavaCallHelper::~JavaCallHelper() {
env->DeleteGlobalRef(instance);}
void JavaCallHelper::onError(int thread,int error){
//主線程
if (thread == THREAD_MAIN){
env->CallVoidMethod(instance,onErrorId,error);
} else{
//子線程
JNIEnv *env;
//獲得屬于我這一個(gè)線程的jnienv
vm->AttachCurrentThread(&env,0);
env->CallVoidMethod(instance,onErrorId,error);
vm->DetachCurrentThread();
}}
void JavaCallHelper::onPrepare(int thread) {
if (thread == THREAD_MAIN){
env->CallVoidMethod(instance,onPrepareId);
} else{
//子線程
JNIEnv *env;
//獲得屬于我這一個(gè)線程的jnienv
vm->AttachCurrentThread(&env,0);
env->CallVoidMethod(instance,onPrepareId);
vm->DetachCurrentThread();
}}
onError 方法中處理我們的錯(cuò)誤信息反射給 Java洋措,如果是主線程 THREAD_MAIN 直接傳遞 Env,如果是子線程就通過 vm->AttachCurrentThread(&env,0);
獲得屬于我這一個(gè)線程的 JNIEnv杰刽,調(diào)用完畢后 DetachCurrentThread菠发。然后在 MyFFmpeg 中 callHelper->onError(THREAD_CHILD,FFMPEG_CAN_NOT_OPEN_URL);
第一個(gè)參數(shù)代表子線程,第二個(gè)參數(shù)代表錯(cuò)誤信息贺嫂。
回到我們的 _prepare 方法中來滓鸠,第二個(gè)方法 avformat_find_stream_info,查看頭文件 int avformat_find_stream_info(AVFormatContext *ic, AVDictionary **options);
這個(gè)方法表示查找媒體中的音視頻流(給 contxt 里的 streams 等成員賦值)第喳。返回小于 0 失敗糜俗,大于等于 0 成功。失敗的話就回調(diào)給 Java 墩弯,調(diào)用這個(gè)方法后吩跋,formatContext 就有值了。這里我們看一下 AVFormatContext 這個(gè)結(jié)構(gòu)體: