一、準(zhǔn)備工作
1.編譯ffmpeg開發(fā)包
參照上一個簡書內(nèi)容
FFmpeg核心模塊
libavformat
用于各種音視頻封裝格式的生成和解析,包括獲取解碼所需信息以生成解碼上下文結(jié)構(gòu)和讀取音視頻幀等功能;音視頻的格式解析協(xié)議,為 libavcodec 分析碼流提供獨(dú)立的音頻或視頻碼流源。
libavcodec
用于各種類型聲音/圖像編解碼;該庫是音視頻編解碼核心,實(shí)現(xiàn)了市面上可見的絕大部分解碼器的功能,libavcodec 庫被其他各大解碼器 ffdshow,Mplayer 等所包含或應(yīng)用。
libavfilter
filter(FileIO、FPS、DrawText)音視頻濾波器的開發(fā),如水印、倍速播放等。
libavutil
包含一些公共的工具函數(shù)的使用庫,包括算數(shù)運(yùn)算 字符操作;
libswresample
原始音頻格式轉(zhuǎn)碼。
libswscale
(原始視頻格式轉(zhuǎn)換)用于視頻場景比例縮放、色彩映射轉(zhuǎn)換;圖像顏色空間或格式轉(zhuǎn)換,如 rgb565,rgb888 等與 yuv420 等之間轉(zhuǎn)換。
libpostproc+libavcodec
2.開發(fā)要用到的直播接口
cate:類別
cate=lol : 英雄聯(lián)盟
cate=acg 二次元
cate=food 美食
更多分類:
https://www.panda.tv/cate?pdt=1.18.pheader-n.2.7prrbrd3cgd
http://api.m.panda.tv/ajax_get_live_list_by_cate?cate=lol&pageno=1&pagenum=1&room=1&version=3.3.1.5978
獲得lol類別的房間
{"errno":0,"errmsg":"","data":{"items":[{"id":"237908","name":"\u8001\u5218\uff1a\u5218\u83b1\u5384\u65af\u6e29\u9152\u65a9\u4e0a\u5355\uff01\u6781\u96501V9","hostid":"24872350","person_num":"211986","classification":{"cname":"\u82f1\u96c4\u8054\u76df","ename":"lol"},"pictures":{"img":"http:\/\/i8.pdim.gs\/90\/2833bd6763a56b25229883dc29c1c505\/w338\/h190.jpg"},"display_type":"1","tag":"","tag_switch":"0","tag_color":"3","style_type":"1","reliable":"1","status":"2","stream_status":"1","createtime":"2015-12-30 04:44:08","start_time":"1535936417","schedule":"1457226000","room_type":"1","lianmai":"3","host_level_info":"{\"val\":4921.297889,\"c_lv\":13,\"c_lv_val\":4161,\"n_lv\":14,\"n_lv_val\":5180,\"plays_day\":1293,\"bamboo_user\":183.230062,\"gift_user\":1519.130498,\"gift_cnt\":1925.965104,\"vip\":0}","top_icon":"0","label":[{"cname":"\u8054\u76df\u7cbe\u82f1","color":"5","ename":"lmjy"}],"room_key":"a565062faa92b2497fba298df9fc22ac","rollinfo":["quiz"],"fans":"0","userinfo":{"rid":24872350,"userName":"","nickName":"\u65cb\u8f6c\u7684\u8001\u5218\u8bfa\u624b","avatar":"http:\/\/i7.pdim.gs\/2edfc92b52d03d3979d0b7d2dceeebd5.jpeg"},"announcement":"","duration":"9615","click_trace":"list","room_activity":{"type":"5","value":"\u7ade\u731c"},"definition_option":{"HD":"1","OD":"1","SD":"1"},"xy_stat":"0","tx_stat":"0","hardware":"2","decoder":{"HD":"","OD":"","SD":""}}],"total":"285","type":{"ename":"lol","cname":"\u82f1\u96c4\u8054\u76df"},"liveswitch":0},"authseq":""}
http://api.m.panda.tv/ajax_get_liveroom_baseinfo?roomid=237908&__version=3.3.1.5978&slaveflag=1&type=json&__plat=android
roomid 第一個請求地址獲得的json中的id
{"errno":0,"errmsg":"","data":{"info":{"hostinfo":{"rid":24872350,"name":"\u65cb\u8f6c\u7684\u8001\u5218\u8bfa\u624b","avatar":"http:\/\/i7.pdim.gs\/dmfd\/200_200_100\/2edfc92b52d03d3979d0b7d2dceeebd5.jpeg","bamboos":"389019586","level":{"val":4921.328189,"c_lv":13,"c_lv_val":4161,"n_lv":14,"n_lv_val":5180,"plays_day":1293,"bamboo_user":183.230062,"gift_user":1519.130498,"gift_cnt":1925.967629,"vip":0},"qq":{"list":[{"qq":"517509252","description":"\u8001\u5218\u8bfa\u624b\u96c6\u4e2d\u247b\u8425","url":"https:\/\/jq.qq.com\/?_wv=1027&k=56TUN1o","opttime":1535080016}],"count":1}},"roominfo":{"id":"237908","name":"\u8001\u5218\uff1a\u5218\u83b1\u5384\u65af\u6e29\u9152\u65a9\u4e0a\u5355\uff01\u6781\u96501V9","type":"1","classification":"\u82f1\u96c4\u8054\u76df","cate":"lol","bulletin":"\u76f4\u64ad\u65f6\u95f4\uff1a9.00-2.00\uff0c\u65b0\u6d6a\u5fae\u535a\uff1a\u65cb\u8f6c\u7684\u8001\u5218\u8bfa\u624b\uff0c\u5929\u8d4b\u7b26\u6587\u90fd\u5728\u91cc\u9762\uff0c\u6ca1\u4e8b\u79c1\u4fe1\u91cc\u9762\u62bd\u5956\u6253\uff01\u8054\u7cfb\u4e3b\u64ad\u52a0\u7fa4\u8054\u7cfb\u7fa4\u4e3b","details":"","person_num":"359640","fans":"660823","pictures":{"img":"http:\/\/i7.pdim.gs\/90\/67a5ebb3a216498ff1286bfbc2bcf412\/w338\/h190.jpg"},"display_type":"1","start_time":"1535936417","end_time":"1535867259","room_type":"1","status":"2","style_type":"1","banned_reason":"\u8fdd\u53cd\u300a\u718a\u732b\u76f4\u64ad\u4e3b\u64ad\u4fe1\u7528\u503c\u5206\u7ea7\u7ba1\u7406\u529e\u6cd5\u300b\u7b2c76\u6761","unlock_time":"1528868625","pk_stat":1,"ngif_switch":"0","remind_content":"","remind_time":"0","remind_status":"0","payBarrageSwitch":"1","cosmicwarSwtich":"0","videojjSwitch":1,"gmpk_stat":0,"is_have_short":"1"},"userinfo":{"rid":0,"ispay":false},"videoinfo":{"name":"dota","time":"10547","stream_addr":{"HD":"1","OD":"1","SD":"1"},"room_key":"a565062faa92b2497fba298df9fc22ac","plflag":"2_3","status":"2","sign":"974067f67e93a6411f5b9d44c6d3c5e8","ts":"&ts=5b8cb0d4&rid=-62409061","hardware":0,"scheme":"http","main":"2_3","xy_stat":"1","tx_stat":"1","ws_stat":"1","kcg_stat":"1","decoder":{"HD":"","OD":"","SD":""},"slaveflag":["14_29"],"p2pconf":[{"14_28":"xy"},{"14_29":"xy"},{"2_3":"xy"},{"2_4":"xy"},{"4_8":"xy"},{"4_7":"xy"},{"15_31":"xy"},{"15_30":"xy"}],"watermark":0},"tabinfo":[{"tab_id":"station","label_cname":"\u8f66\u7ad9","tab_rank":"3","is_default_click":0,"link":"https:\/\/medusa.m.panda.tv\/station.html?roomid=237908&hostid=24872350&hostname=%E6%97%8B%E8%BD%AC%E7%9A%84%E8%80%81%E5%88%98%E8%AF%BA%E6%89%8B"}],"skininfo":[],"groupinfo":{"groupid":"101317","name":"\u90d1\u5dde\u5927\u5b66","sp_name":"\u90d1\u5927"}}},"authseq":""}
http://pl3.live.panda.tv/live_panda/a565062faa92b2497fba298df9fc22ac_mid.flv?sign=974067f67e93a6411f5b9d44c6d3c5e8&time=&ts=5b8cae1e&rid=-62264111
a565062faa92b2497fba298df9fc22ac: room_key
974067f67e93a6411f5b9d44c6d3c5e8: sign
time=后面拼上ts &ts=5b8cb0d4&rid=-62409061
3.項(xiàng)目開發(fā)架構(gòu)流程圖

程序結(jié)構(gòu).png
二、ffmpeg播放器1-直播流信息獲取,打開解碼器
1.ffmpeg 獲取音視頻流信息,打開解碼器流程
// AVFormatContext 包含了 視頻的 信息(寬、高等)
formatContext = 0;
//1. 初始化網(wǎng)絡(luò) 讓ffmpeg能夠使用網(wǎng)絡(luò)
avformat_network_init();
//2.打開媒體地址(文件地址、直播地址)
//ret != 0 失敗,文件路徑不對 手機(jī)沒網(wǎng) 0成功
int ret = avformat_open_input(&formatContext,dataSource,0,0);
//3.查找媒體中的 音視頻流 (給 contxt里的 streams等成員賦值) <0失敗 ==0成功
ret = avformat_find_stream_info(formatContext,0);
//4.共有幾段視頻和音頻 循環(huán)處理
formatContext->nb_streams 流的個數(shù)
//5.循環(huán)處理流
//5.1 獲取到這段流,可能代表是一個視頻 也可能代表是一個音頻
AVStream *stream = formatContext->streams[i];
//5.2 獲取到流的參數(shù),包含了 解碼 這段流 的各種參數(shù)信息(寬、高、碼率、幀率)
AVCodecParameters *codecpar = stream->codecpar;
//5.3 通過 當(dāng)前流 使用的 編碼方式,查找解碼器
AVCodec *dec = avcodec_find_decoder(codecpar->codec_id);
//5.4 獲得解碼器上下文
AVCodecContext *context = avcodec_alloc_context3(dec);
//5.5 設(shè)置上下文內(nèi)的一些參數(shù) (context->width)
//context->width = codecpar->width;
//context->height = codecpar->height;
ret = avcodec_parameters_to_context(context,codecpar); //<0失敗 ==0成功
//5.6 打開解碼器
ret = avcodec_open2(context,dec,0);
//5.7 判斷流是音頻還是視頻等進(jìn)行分別處理
//音頻
if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
audioChannel = new AudioChannel;
}
//視頻
else if(codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
videoChannel = new VideoChannel;
}
2.相關(guān)知識整理
3.代碼實(shí)現(xiàn):
創(chuàng)建 DNPlayer.java,用于調(diào)用so中的方法
創(chuàng)建 mylog.h, 用于定義一些宏
創(chuàng)建 DNFFmpeg.h & .cpp ,用于提供方法給 native-lib.cpp 調(diào)用
創(chuàng)建 JavaCallHelper.h & .cpp ,用于so調(diào)用java的方法(即回調(diào))
創(chuàng)建 AudioChannel.h & .cpp ,用于音頻開發(fā)
創(chuàng)建 VideoChannel& .cpp ,用于視頻開發(fā)
1. DNPlayer.java:
static {
System.loadLibrary("native-lib");
}
setSurfaceView(SurfaceView surfaceView) //設(shè)置要顯示的畫布
setDataSource(String dataSource)//設(shè)置播放的文件 或直播地址
prepare()// 準(zhǔn)備工作 --> native native_prepare(String dataSource);
start()// 開始播放 --> native native_start();
...
2. mylog.h
#include <android/log.h>
#ifndef JNITEST_MYLOG_H
#define JNITEST_MYLOG_H
#define LOGE(TAG,...) __android_log_print(ANDROID_LOG_ERROR,TAG, __VA_ARGS__)
#define LOGFFE(...) __android_log_print(ANDROID_LOG_ERROR,"FFMPEG", __VA_ARGS__)
//宏函數(shù)
#define DELETE(obj) if(obj){ delete obj; obj = 0; }
//標(biāo)記線程 因?yàn)樽泳€程需要attach
#define THREAD_MAIN 1
#define THREAD_CHILD 2
//錯誤代碼
//打不開視頻
#define FFMPEG_CAN_NOT_OPEN_URL 1
//找不到流媒體
#define FFMPEG_CAN_NOT_FIND_STREAMS 2
//找不到解碼器
#define FFMPEG_FIND_DECODER_FAIL 3
//無法根據(jù)解碼器創(chuàng)建上下文
#define FFMPEG_ALLOC_CODEC_CONTEXT_FAIL 4
//根據(jù)流信息 配置上下文參數(shù)失敗
#define FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL 6
//打開解碼器失敗
#define FFMPEG_OPEN_DECODER_FAIL 7
//沒有音視頻
#define FFMPEG_NOMEDIA 8
#endif //
3.native-lib中生成對應(yīng)的方法供java代碼調(diào)用
#include <jni.h>
#include <string>
#include "DNFFmpeg.h"
DNFFmpeg *ffmpeg = 0;
JavaVM *javaVm = 0;
int JNI_OnLoad(JavaVM *vm, void *r) {
javaVm = vm;
return JNI_VERSION_1_6;
}
extern "C"
JNIEXPORT void JNICALL
Java_com_dongnao_player_DNPlayer_native_1prepare(JNIEnv *env, jobject instance,
jstring dataSource_) {
const char *dataSource = env->GetStringUTFChars(dataSource_, 0);
//創(chuàng)建播放器和回調(diào)
JavaCallHelper *helper = new JavaCallHelper(javaVm, env, instance);
ffmpeg = new DNFFmpeg(helper, dataSource);
ffmpeg->prepare();
env->ReleaseStringUTFChars(dataSource_, dataSource);
}
4.JavaCallHelper.h & JavaCallHelper.cpp
JavaCallHelper.h:
#ifndef MYFFMPEGPLAYER_JAVACALLHELPER_H
#define MYFFMPEGPLAYER_JAVACALLHELPER_H
#include <jni.h>
#include "mylog.h"
class JavaCallHelper {
public:
JavaCallHelper(JavaVM *pVM,
JNIEnv *pEnv,
jobject pJobject);
~JavaCallHelper();
//prepare方法 回調(diào)java方法
// 準(zhǔn)備工作完成 解碼器打開成功
void onPrepare(int thread);
//準(zhǔn)備工作完成 解碼器打開失敗
void onPreError(int thread,int errorCode);
private:
JavaVM *vm;
JNIEnv *env;
jobject instance;
jmethodID onErrorId;
jmethodID onPrepareId;
};
JavaCallHelper.cpp:
#include "JavaCallHelper.h"
JavaCallHelper::JavaCallHelper(JavaVM *pVM, JNIEnv *pEnv, jobject pJobject) {
this->vm = pVM;
this->env = pEnv;
// 一旦涉及到j(luò)object 跨方法 跨線程 就需要創(chuàng)建全局引用
this->instance = env->NewGlobalRef(pJobject);
jclass clz = pEnv->GetObjectClass(pJobject);
onErrorId = pEnv->GetMethodID(clz,"onPreError","(I)V");
onPrepareId = pEnv->GetMethodID(clz,"onPrepare","()V");
}
JavaCallHelper::~JavaCallHelper() {
env->DeleteGlobalRef(instance);
}
// 準(zhǔn)備工作完成 解碼器打開成功
void JavaCallHelper::onPrepare(int thread) {
if(thread == THREAD_MAIN){
// 主線程
env->CallVoidMethod(instance,onPrepareId);
}else{
// 子線程
JNIEnv *env;
vm->AttachCurrentThread(&env,0);
env->CallVoidMethod(instance,onPrepareId);
vm->DetachCurrentThread();
}
}
void JavaCallHelper::onPreError(int thread, int errorCode) {
//主線程
if (thread == THREAD_MAIN){
env->CallVoidMethod(instance,onErrorId,errorCode);
} else{
//子線程
JNIEnv *env;
//獲得屬于我這一個線程的jnienv
vm->AttachCurrentThread(&env,0);
env->CallVoidMethod(instance,onErrorId,errorCode);
vm->DetachCurrentThread();
}
}
5.DNFFmpeg.h & DNFFmpeg.cpp
DNFFmpeg.h:
#include "VideoChannel.h"
extern "C" {
#include <libavformat/avformat.h>
}
class DNFFmpeg {
public:
DNFFmpeg(JavaCallHelper* callHelper,const char* dataSource);
~DNFFmpeg();
// 播放器準(zhǔn)備工作
void prepare();
// 線程中調(diào)用該方法,用于實(shí)現(xiàn)具體解碼音視頻代碼
void _prepare();
private:
// 音視頻地址
char *dataSource;
// 解碼器打開線程
pthread_t pid;
// 解碼器上下文
AVFormatContext *formatContext = 0;
// ...
JavaCallHelper* callHelper = 0;
AudioChannel *audioChannel = 0;
VideoChannel *videoChannel = 0;
};
#endif //MYFFMPEGPLAYER_DNFFMPEG_H
DNFFmpeg.cpp:
#include "DNFFmpeg.h"
// 解碼器打開線程
void* task_prepare(void* args){
DNFFmpeg *dnfFmpeg = static_cast<DNFFmpeg *>(args);
//調(diào)用解碼器打開方法
dnfFmpeg->_prepare();
return 0;
}
DNFFmpeg::DNFFmpeg(JavaCallHelper *callHelper, const char *dataSource){
this->callHelper = callHelper;
//防止 dataSource參數(shù) 指向的內(nèi)存被釋放
this->dataSource = new char[strlen(dataSource)+1];
strcpy(this->dataSource,dataSource);
}
DNFFmpeg::~DNFFmpeg() {
//釋放
DELETE(dataSource);
DELETE(callHelper);
}
void DNFFmpeg::prepare() {
// 創(chuàng)建一個解碼器打開的線程
pthread_create(&pid,NULL,task_prepare,this);
}
// 解碼器打開的實(shí)現(xiàn)方法
void DNFFmpeg::_prepare() {
// 初始化網(wǎng)絡(luò) 讓ffmpeg能夠使用網(wǎng)絡(luò)
avformat_network_init();
//1、打開媒體地址(文件地址、直播地址)
// AVFormatContext 包含了 視頻的 信息(寬、高等)
formatContext = 0;
//文件路徑不對 手機(jī)沒網(wǎng)
int ret = avformat_open_input(&formatContext,dataSource,0,0);
//ret不為0表示 打開媒體失敗
if(ret != 0){
LOGFFE("打開媒體失敗:%s",av_err2str(ret));
callHelper->onPreError(THREAD_CHILD,FFMPEG_CAN_NOT_OPEN_URL);
return;
}
//2、查找媒體中的 音視頻流 (給 contxt里的 streams等成員賦值)
ret = avformat_find_stream_info(formatContext,0);
// 小于0 則失敗
if (ret < 0){
LOGFFE("查找流失敗:%s",av_err2str(ret));
callHelper->onPreError(THREAD_CHILD,FFMPEG_CAN_NOT_FIND_STREAMS);
return;
}
//nb_streams :幾個流(幾段視頻/音頻)
for (int i = 0; i < formatContext->nb_streams; ++i) {
//可能代表是一個視頻 也可能代表是一個音頻
AVStream *stream = formatContext->streams[i];
//包含了 解碼 這段流 的各種參數(shù)信息(寬、高、碼率、幀率)
AVCodecParameters *codecpar = stream->codecpar;
//無論視頻還是音頻都需要干的一些事情(獲得解碼器)
// 1、通過 當(dāng)前流 使用的 編碼方式,查找解碼器
AVCodec *dec = avcodec_find_decoder(codecpar->codec_id);
if(dec == NULL){
LOGFFE("查找解碼器失敗:%s",av_err2str(ret));
callHelper->onPreError(THREAD_CHILD,FFMPEG_FIND_DECODER_FAIL);
return;
}
//2、獲得解碼器上下文
AVCodecContext *context = avcodec_alloc_context3(dec);
if(context == NULL){
LOGFFE("創(chuàng)建解碼上下文失敗:%s",av_err2str(ret));
callHelper->onPreError(THREAD_CHILD,FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
return;
}
//3、設(shè)置上下文內(nèi)的一些參數(shù) (context->width)
// context->width = codecpar->width;
// context->height = codecpar->height;
ret = avcodec_parameters_to_context(context,codecpar);
//失敗
if(ret < 0){
LOGFFE("設(shè)置解碼上下文參數(shù)失敗:%s",av_err2str(ret));
callHelper->onPreError(THREAD_CHILD,FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);
return;
}
// 4、打開解碼器
ret = avcodec_open2(context,dec,0);
if (ret != 0){
LOGFFE("打開解碼器失敗:%s",av_err2str(ret));
callHelper->onPreError(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){
LOGFFE("沒有音視頻");
callHelper->onPreError(THREAD_CHILD,FFMPEG_NOMEDIA);
return;
}
// 準(zhǔn)備完了 通知java 你隨時可以開始播放
callHelper->onPrepare(THREAD_CHILD);
}
重點(diǎn) _prepare()方法,音視頻解碼器打開,到此,我們的第一步解碼器打開就完成了