Android SurfaceView播放RTMP流
2020-06-17 本文已影响0人
星云春风
1.编译ffmpeg 和rtmp 静态库,并配置环境
2.JAVA层实现SurfaceHolder.Callback
- surfaceChanged回调中传递Surface给Native层去操作
extern "C"
JNIEXPORT void JNICALL
Java_com_xia_ndk_1player_1code_MyPlayer_setSurfaceNative(JNIEnv *env, jobject thiz,
jobject surface) {
// TODO: implement setSurfaceNative()
pthread_mutex_lock(&mutex);
if (nativeWindow) {
ANativeWindow_release(nativeWindow);
nativeWindow = 0;
}
// 创建新的窗口用于视频显示
nativeWindow = ANativeWindow_fromSurface(env, surface);
pthread_mutex_unlock(&mutex);
}
- 定义prepareNative(String dataSource)方法给Native层传递流地址或者流文件
extern "C"
JNIEXPORT void JNICALL
Java_com_xia_ndk_1player_1code_MyPlayer_prepareNative(JNIEnv *env, jobject thiz,
jstring data_source) {
// TODO: implement prepareNative()
const char *dataSource = env->GetStringUTFChars(data_source, NULL);
JNICallback *jniCallback = new JNICallback(::myJavaVm, env, thiz);
//面向对象 创建MyPlayer
myPlayer = new MyPlayer(dataSource, jniCallback);
myPlayer->setRenderCallback(renderFrame);
myPlayer->prepare();
env->ReleaseStringUTFChars(data_source, dataSource);
}
3.Native中获取流地址或者流文件,JNICallback回调用于跟Java层进行传递信息
MyPlayer::MyPlayer(const char *data_source, JNICallback *pCallback) {
//拿到的是悬空
//this->data_source = data_source;
//长度不对,需要加1 c和C++的区别
this->data_source = new char[strlen(data_source) + 1];
strcpy(this->data_source, data_source);
this->jniCallback = pCallback;
}
4. 开始准备工作
- 创建子线程进行准备
void *customTaskPrepareThread(void *pVoid) {
MyPlayer *myPlayer = static_cast<MyPlayer *>(pVoid);
//创建异步函数
myPlayer->prepare_();
return 0;
}
//准备工作 ,(拆包裹 解码 解封装 解复用)(音频流 视频流 字幕流。。。等) 主线程
void MyPlayer::prepare() {
//创建异步线程
pthread_create(&this->pid_prepare, 0, customTaskPrepareThread, this);
}
- 2 .获取媒体上下文
this->avFormatContext = avformat_alloc_context();
- 设置字典
//此字典 能决定打开的需求
AVDictionary *dictionary = 0;
//打开文件花的时长 单位是微秒
av_dict_set(&dictionary, "timeout", "5000000", 0);
- 初始化网络
// 初始化网络
avformat_network_init();
- 5.打开流媒体文件
/**
* 媒体上下文
* 数据源
* 数据格式
* 数据字典
* 判断是不是包裹,如果是石头(被损坏的数据),就无法解析
*/
int ret = avformat_open_input(&avFormatContext, this->data_source, 0, &dictionary);
- 释放字典
av_dict_free(&dictionary); // 释放字典
- 7.判断打开流媒体文件是否成功
//返回 0是成功
if (ret) {
// TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层,数据流被损坏了
if (jniCallback) {
this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_CAN_NOT_OPEN_URL);
}
return;
}
- 8.寻找媒体格式中的(音频 视频 字幕)
//1.寻找媒体格式中的(音频 视频 字幕) ,不给字典是因为不需要设置额外的配置 2.给媒体上下文赋值
ret = avformat_find_stream_info(this->avFormatContext, 0);
- 9.判断是否有找到流媒体
if (ret < 0) {
// TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层,找不到流媒体
if (jniCallback) {
this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_CAN_NOT_FIND_STREAMS);
}
return;
}
- 循环遍历流媒体上下文中的流,获取流数据, 流0(视频) 流1 (音频) 流2(字幕) 等等
for (int i = 0; i < this->avFormatContext->nb_streams; ++i) {
//获取媒体流
AVStream *stream = this->avFormatContext->streams[i];
//获取编解码器
AVCodecParameters *codecParameters = stream->codecpar;
//编解码器ID
enum AVCodecID codecId = codecParameters->codec_id;
//拿到编解码器
AVCodec *codec = avcodec_find_decoder(codecId);
if (!codec) {
// TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层 找不到解码器
if (jniCallback) {
this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_FIND_DECODER_FAIL);
}
return;
}
//为了解码,需要解码器上下文h
AVCodecContext *codecContext = avcodec_alloc_context3(codec);
if (!codecContext) {
// TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层, 获取解码器上下文失败
if (jniCallback) {
this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_ALLOC_CODEC_CONTEXT_FAIL);
}
return;
}
//解码器上下文 设置参数
ret = avcodec_parameters_to_context(codecContext, codecParameters);
if (ret < 0) {
// TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层, 解码器上下文设置失败
if (jniCallback) {
this->jniCallback->onErrorAction(THREAD_CHILD,
FFMPEG_CODEC_CONTEXT_PARAMETERS_FAIL);
}
return;
}
//打开解码器
ret = avcodec_open2(codecContext, codec, 0);
if (ret) {
//0是成功,非0为true
// TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层, 打开解码器失败
if (jniCallback) {
this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_OPEN_DECODER_FAIL);
}
return;
}
//使用解码器
//区分音频 视频 通道
if (codecParameters->codec_type == AVMEDIA_TYPE_AUDIO) {
this->audioChannel = new AudioChannel(i, codecContext);
} else if (codecParameters->codec_type == AVMEDIA_TYPE_VIDEO) {
//现在很多字幕流放在视频轨里一起了。
this->videoChannel = new VideoChannel(i, codecContext);
videoChannel->setRenderCallback(renderCallback);
} else {
// TODO xia chen hui 2020/6/13 19:36 写JNI回调,通知java层, 没有音视频
if (jniCallback) {
this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_NOMEDIA);
}
}
}
- 11 .准备完毕,进行播放操作
if (!audioChannel && !videoChannel) {
// 把错误信息,告诉给Java层去(回调给Java)
this->jniCallback->onErrorAction(THREAD_CHILD, FFMPEG_NOMEDIA);
return;
}
//告诉java层 ,准备OK了
if (jniCallback) {
this->jniCallback->onPrepared(THREAD_CHILD);
}
5. 视频播放操作,包括解码和播放两步
- 开启子线把未解码的数据放入队列packets中
//未解码的格式,保存在AVPacket中
AVPacket *packet = av_packet_alloc();
//执行这个 ,packet就可以获取值了
int ret = av_read_frame(this->avFormatContext, packet);
if (!ret) { //ret == 0
//把已经得到的Packet 放入队列中
//先要判断是否是音频 还是视频
if (videoChannel && videoChannel->stream_index == packet->stream_index) {
// 说明是视频
this->videoChannel->packets.push(packet);
} else if (audioChannel && audioChannel->stream_index == packet->stream_index) {
//音频
this->audioChannel->packets.push(packet);
}
} else if (ret == AVERROR_EOF) {
//文件末尾 ,流读完了
} else {
//代表失败了
break;
}
- 队列中取出数据,进行解码以及播放
// 从队列中取出 1.解码 2. 播放
void VideoChannel::start() {
this->isPlaying = 1;
// 存放未解锁的队列 开始工作。
this->packets.setFlag(1);
//存放已解码的队列,开始工作
this->frames.setFlag(1);
//解码的线程
pthread_create(&pid_video_decode, 0, task_video_decode, this);
// 播放的线程
pthread_create(&pid_video_player, 0, task_video_player, this);
}
- 3 解码操作
//异步线程,开始解码
void VideoChannel::video_decode() {
//取出未解码的队列数据
AVPacket *packet;
while (isPlaying) {
// 消费速度比生成速度慢(生成100,只消费10个,这样队列会爆)
// 内存泄漏点2,解决方案:控制队列大小
if (isPlaying && frames.queueSize() > 100) {
// 休眠 等待队列中的数据被消费
av_usleep(10 * 1000);
continue;
}
int ret = this->packets.pop(packet);
if (!isPlaying) {
break;
}
if (!ret) {
continue;
}
//可以未解码的视频数据包
ret = avcodec_send_packet(this->avCodecContext, packet);
if (ret) {
//失败了
break;
}
//创建原始数据 ,开始把音频AAC 视频H264转变成音频PCM 视频YUV了
AVFrame *avFrame = av_frame_alloc();
ret = avcodec_receive_frame(this->avCodecContext, avFrame);
if (ret == AVERROR(EAGAIN)) {
//代表帧取的不完整 如果是I帧(完整的一帧)就不会进入,如果是P帧 B帧 就会进入这里
continue; //重新取,直到取到完整的帧为止
} else if (ret != 0) {
// TODO xia chen hui 2020/6/14 23:38 做释放工作
break;
}
//取到了原始数据
this->frames.push(avFrame);
}
// TODO xia chen hui 2020/6/14 23:39 出了循环 ,需要释放
releaseAVPacket(&packet);
}
- 4 .播放操作
//异步线程 开始播放
void VideoChannel::video_player() {
// yuv最原始的视频帧数据,但是不能显示在屏幕上(Android IOS SDL等)
//yuv 转变成rgba 才能显示
// TODO xia chen hui 2020/6/15 0:04 1.转换的上下文 ,把yunv转为rgba ,SwsContext是视频
SwsContext *swsContext = sws_getContext(
//原始的一层 宽 高 格式
this->avCodecContext->width, this->avCodecContext->height,
this->avCodecContext->pix_fmt,
//目标最终要显示到屏幕的信息 ,最好和原始的保持一致
this->avCodecContext->width, this->avCodecContext->height, AV_PIX_FMT_RGBA,
//渲染的速率 ,这个是比较常用的
SWS_BILINEAR,
NULL, NULL, NULL
);
// TODO xia chen hui 2020/6/15 0:04 2.给转换后的数据 rgba 这种申请内存
uint8_t *dst_data[4];
int dst_linesize[4];
AVFrame *avFrame = 0;
av_image_alloc(dst_data, dst_linesize,
avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA, 1);
// TODO xia chen hui 2020/6/15 0:07 3. 原始数据转换 (队列中拿到原始数据,一帧一帧的转换未rgba,一帧一帧渲染到屏幕)
int ret;
while (isPlaying) {
ret = frames.pop(avFrame);
if (!isPlaying) {
//停止播放,就跳出循环,出了循环就需要释放
}
if (!ret) {
//失败了就继续
continue;
}
//avFrame->data是yuv原始数据 dst_data是rgba格式的数据
sws_scale(swsContext, avFrame->data,
avFrame->linesize, 0, avCodecContext->height, dst_data, dst_linesize);
//渲染 方式有2种,1 渲染一帧图像(宽 高 数据) 这个会回调native-lib中的renderFrame函数
this->renderCallback(dst_data[0], avCodecContext->width, avCodecContext->height,
dst_linesize[0]);
releaseAVFrame(&avFrame);
}
releaseAVFrame(&avFrame);
isPlaying = 0;
av_freep(dst_data[0]);
sws_freeContext(swsContext);
}
- 渲染屏幕
/**
* 专门渲染的函数
* @param src_data 解码后的视频 rgba 数据
* @param width 宽信息
* @param height 高信息
* @param src_liinesize 行数size相关信息
*/
void renderFrame(uint8_t *src_data, int width, int height, int src_liinesize) {
pthread_mutex_lock(&mutex);
if (!nativeWindow) {
pthread_mutex_unlock(&mutex);
}
// 设置窗口属性
ANativeWindow_setBuffersGeometry(nativeWindow, width, height, WINDOW_FORMAT_RGBA_8888);
ANativeWindow_Buffer windowBuffer;
if (ANativeWindow_lock(nativeWindow, &windowBuffer, 0)) {
ANativeWindow_release(nativeWindow);
nativeWindow = 0;
pthread_mutex_unlock(&mutex);
return;
}
// 填数据到buffer,其实就是修改数据
uint8_t *dst_data = static_cast<uint8_t *>(windowBuffer.bits);
int lineSize = windowBuffer.stride * 4; // RGBA ,每一个像素点xRGBA
// 下面就是逐行Copy了
for (int i = 0; i < windowBuffer.height; ++i) {
// 一行一行的copy到Android 屏幕上
memcpy(dst_data + i * lineSize, src_data + i * src_liinesize, lineSize);
}
ANativeWindow_unlockAndPost(nativeWindow);
pthread_mutex_unlock(&mutex);
}
- 释放队列
// 注意:由于是父类,析构函数,必须是虚函数
virtual ~BaseChannel() {
packets.clearQueue();
frames.clearQueue();
}
/**
* 释放AVFrame 队列
* @param avFrame
*/
static void releaseAVFrame(AVFrame **avFrame) {
if (avFrame) {
av_frame_free(avFrame);
*avFrame = 0;
}
}
/**
* 释放AVPacket 队列
* @param avPacket
*/
static void releaseAVPacket(AVPacket **avPacket) {
if (avPacket) {
av_packet_free(avPacket);
*avPacket = 0;
}
}