一、准备工作
1.编译ffmpeg开发包
参照上一个简书内容
FFmpeg核心模块
libavformat
用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能;音视频的格式解析协议,为 libavcodec 分析码流提供独立的音频或视频码流源。
libavcodec
用于各种类型声音/图像编解码;该库是音视频编解码核心,实现了市面上可见的绝大部分解码器的功能,libavcodec 库被其他各大解码器 ffdshow,Mplayer 等所包含或应用。
libavfilter
filter(FileIO、FPS、DrawText)音视频滤波器的开发,如水印、倍速播放等。
libavutil
包含一些公共的工具函数的使用库,包括算数运算 字符操作;
libswresample
原始音频格式转码。
libswscale
(原始视频格式转换)用于视频场景比例缩放、色彩映射转换;图像颜色空间或格式转换,如 rgb565,rgb888 等与 yuv420 等之间转换。
libpostproc+libavcodec
2.开发要用到的直播接口
cate:类别
cate=lol : 英雄联盟
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.项目开发架构流程图
程序结构.png二、ffmpeg播放器1-直播流信息获取,打开解码器
1.ffmpeg 获取音视频流信息,打开解码器流程
// AVFormatContext 包含了 视频的 信息(宽、高等)
formatContext = 0;
//1. 初始化网络 让ffmpeg能够使用网络
avformat_network_init();
//2.打开媒体地址(文件地址、直播地址)
//ret != 0 失败,文件路径不对 手机没网 0成功
int ret = avformat_open_input(&formatContext,dataSource,0,0);
//3.查找媒体中的 音视频流 (给 contxt里的 streams等成员赋值) <0失败 ==0成功
ret = avformat_find_stream_info(formatContext,0);
//4.共有几段视频和音频 循环处理
formatContext->nb_streams 流的个数
//5.循环处理流
//5.1 获取到这段流,可能代表是一个视频 也可能代表是一个音频
AVStream *stream = formatContext->streams[i];
//5.2 获取到流的参数,包含了 解码 这段流 的各种参数信息(宽、高、码率、帧率)
AVCodecParameters *codecpar = stream->codecpar;
//5.3 通过 当前流 使用的 编码方式,查找解码器
AVCodec *dec = avcodec_find_decoder(codecpar->codec_id);
//5.4 获得解码器上下文
AVCodecContext *context = avcodec_alloc_context3(dec);
//5.5 设置上下文内的一些参数 (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 判断流是音频还是视频等进行分别处理
//音频
if (codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
audioChannel = new AudioChannel;
}
//视频
else if(codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
videoChannel = new VideoChannel;
}
2.相关知识整理
3.代码实现:
创建 DNPlayer.java,用于调用so中的方法
创建 mylog.h, 用于定义一些宏
创建 DNFFmpeg.h & .cpp ,用于提供方法给 native-lib.cpp 调用
创建 JavaCallHelper.h & .cpp ,用于so调用java的方法(即回调)
创建 AudioChannel.h & .cpp ,用于音频开发
创建 VideoChannel& .cpp ,用于视频开发
1. DNPlayer.java:
static {
System.loadLibrary("native-lib");
}
setSurfaceView(SurfaceView surfaceView) //设置要显示的画布
setDataSource(String dataSource)//设置播放的文件 或直播地址
prepare()// 准备工作 --> 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__)
//宏函数
#define DELETE(obj) if(obj){ delete obj; obj = 0; }
//标记线程 因为子线程需要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
//无法根据解码器创建上下文
#define FFMPEG_ALLOC_CODEC_CONTEXT_FAIL 4
//根据流信息 配置上下文参数失败
#define FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL 6
//打开解码器失败
#define FFMPEG_OPEN_DECODER_FAIL 7
//没有音视频
#define FFMPEG_NOMEDIA 8
#endif //
3.native-lib中生成对应的方法供java代码调用
#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);
//创建播放器和回调
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方法 回调java方法
// 准备工作完成 解码器打开成功
void onPrepare(int thread);
//准备工作完成 解码器打开失败
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;
// 一旦涉及到jobject 跨方法 跨线程 就需要创建全局引用
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);
}
// 准备工作完成 解码器打开成功
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();
// 播放器准备工作
void prepare();
// 线程中调用该方法,用于实现具体解码音视频代码
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);
//调用解码器打开方法
dnfFmpeg->_prepare();
return 0;
}
DNFFmpeg::DNFFmpeg(JavaCallHelper *callHelper, const char *dataSource){
this->callHelper = callHelper;
//防止 dataSource参数 指向的内存被释放
this->dataSource = new char[strlen(dataSource)+1];
strcpy(this->dataSource,dataSource);
}
DNFFmpeg::~DNFFmpeg() {
//释放
DELETE(dataSource);
DELETE(callHelper);
}
void DNFFmpeg::prepare() {
// 创建一个解码器打开的线程
pthread_create(&pid,NULL,task_prepare,this);
}
// 解码器打开的实现方法
void DNFFmpeg::_prepare() {
// 初始化网络 让ffmpeg能够使用网络
avformat_network_init();
//1、打开媒体地址(文件地址、直播地址)
// AVFormatContext 包含了 视频的 信息(宽、高等)
formatContext = 0;
//文件路径不对 手机没网
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];
//包含了 解码 这段流 的各种参数信息(宽、高、码率、帧率)
AVCodecParameters *codecpar = stream->codecpar;
//无论视频还是音频都需要干的一些事情(获得解码器)
// 1、通过 当前流 使用的 编码方式,查找解码器
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("创建解码上下文失败:%s",av_err2str(ret));
callHelper->onPreError(THREAD_CHILD,FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
return;
}
//3、设置上下文内的一些参数 (context->width)
// context->width = codecpar->width;
// context->height = codecpar->height;
ret = avcodec_parameters_to_context(context,codecpar);
//失败
if(ret < 0){
LOGFFE("设置解码上下文参数失败:%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;
}
// 准备完了 通知java 你随时可以开始播放
callHelper->onPrepare(THREAD_CHILD);
}
重点 _prepare()方法,音视频解码器打开,到此,我们的第一步解码器打开就完成了
网友评论