这一步是先做视频播放
首先在页面加入
<SurfaceView
android:id="@+id/surface_view"
android:layout_width="match_parent"
android:layout_height="200dp"/>
然后把surfaceView 传入给DerryPlayer,让这个中间层做一些处理;
public void setSurfaceView(SurfaceView surfaceView) {
if (this.mSurfaceHolder != null){
mSurfaceHolder.removeCallback(this);
}
this.mSurfaceHolder = surfaceView.getHolder();
this.mSurfaceHolder.addCallback(this);
}
surfaceHolder回调
public class DerryPlayer implements SurfaceHolder.Callback{
@Override
public void surfaceCreated(@NonNull SurfaceHolder holder) {
}
@Override
public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
// 这里把surface给native层,让它去绘制图像
setSurfaceNative(holder.getSurface());
}
@Override
public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
}
}
这里有个坑,如果surfaceview 是写在xml中的,那意味着界面初始化就要设置回调,不然后面设置回调
this.mSurfaceHolder.addCallback(this);
回调将不会再出发,看其它资料说是生命周期的原因;
native-lib.cpp中初始化window代码:
extern "C"
JNIEXPORT void JNICALL
Java_com_game_demondk_DerryPlayer_setSurfaceNative(JNIEnv *env, jobject thiz, jobject surface) {
pthread_mutex_lock(&mutex);
if (window){
ANativeWindow_release(window);
window = nullptr;
}
//创建新的窗口,用于视频显示
window = ANativeWindow_fromSurface(env,surface);
pthread_mutex_unlock(&mutex);
}
创建一个基础类,作为音频和视频的基类,用于处理和保存相同类型数据;
BaseChannel.h代码
#ifndef DEMONDK_BASECHANNEL_H
#define DEMONDK_BASECHANNEL_H
#include "safe_queue.h"
#include "JINCallbackHelper.h"
#include "log4c.h" // 日志
extern "C" {
#include "ffmpeg/include/libavcodec/avcodec.h"
#include "ffmpeg/include/libavutil/time.h"
};
class BaseChannel{
public:
int stream_index;//音频或视频的下标
SafeQueue<AVPacket *> packets;
SafeQueue<AVFrame *> frames;
bool isPlaying;//音频和视频都会有的标记
AVCodecContext *avCodecContext = 0;//音频 视频 都需要的 解码器上下文
AVRational time_base;
JINCallbackHelper *jniCallbackHelper;
void setJNICallbackHelper(JINCallbackHelper *jinCallbackHelper){
this->jniCallbackHelper = jinCallbackHelper;
}
BaseChannel(int stream_index,AVCodecContext *avCodecContext,AVRational time_base):
stream_index(stream_index),
avCodecContext(avCodecContext),
time_base(time_base){
packets.setReleaseCallback(releaseAVPacket);
frames.setReleaseCallback(releaseAVFrame);
}
//父类析构一定要加virtual
virtual ~BaseChannel(){
packets.clear();
frames.clear();
}
/**
* 释放 队列中 所有的 AVPacket *
* @param packet
*/
static void releaseAVPacket(AVPacket **pPacket){
if (pPacket){
av_packet_free(pPacket);
*pPacket = 0;
}
}
/**
* 释放 队列中 所有的 AVFrame *
* @param packet
*/
static void releaseAVFrame(AVFrame **frame){
if (frame){
av_frame_free(frame);
*frame = 0;
}
}
};
#endif //DEMONDK_BASECHANNEL_H
视频类代码 VideoChannel.h,分别用两个队列处理保存AVPacket和AVFrame;
//
// Created by DELL on 2021/8/23.
//
#ifndef DEMONDK_VIDEOCHANNEL_H
#define DEMONDK_VIDEOCHANNEL_H
#include "BaseChannel.h"
#include "AudioChannel.h"
#include "util.h"
extern "C" {
#include <libswscale/swscale.h>
#include <libavutil/avutil.h>
#include <libavutil/imgutils.h>
};
typedef void (*RenderCallback)(uint8_t *,int ,int,int);
class VideoChannel : public BaseChannel{
private:
pthread_t pid_video_decode;
pthread_t pid_video_play;
RenderCallback renderCallback;
int fps;
AudioChannel *audioChannel = 0;
public:
VideoChannel(int stream_index, AVCodecContext *codecContext, AVRational timeBase, int i);
~VideoChannel();
void start();
void stop();
void video_decode();
void video_play();
void setRenderCallball(RenderCallback renderCallback1);
void serAudioChannel(AudioChannel *audioChannel);
};
#endif //DEMONDK_VIDEOCHANNEL_H
VideoChannel.cpp代码:
//
// Created by DELL on 2021/8/23.
//
#include <android/log.h>
#include "VideoChannel.h"
void VideoChannel::setRenderCallball(RenderCallback renderCallback) {
this->renderCallback = renderCallback;
}
/**
* 回调是否frame,在队列里面不知道具体是AVFrame还是AVPacket
*
* @param queue
*/
void dropAVFrame(queue<AVFrame *> &queue){
if (!queue.empty()){
AVFrame *frame = queue.front();
BaseChannel::releaseAVFrame(&frame);
queue.pop();
}
}
/**
* 丢包 AVPacket * 压缩包 考虑关键帧
* @param q
*/
void dropAVPacket(queue<AVPacket *> &q) {
while (!q.empty()) {
AVPacket *pkt = q.front();
if (pkt->flags != AV_PKT_FLAG_KEY) { // 非关键帧,可以丢弃
BaseChannel::releaseAVPacket(&pkt);
q.pop();
} else {
break; // 如果是关键帧,不能丢,那就结束
}
}
}
VideoChannel::VideoChannel(int stream_index, AVCodecContext *codecContext, AVRational time_base,
int fps)
: BaseChannel(stream_index, codecContext, time_base),
fps(fps)
{
frames.setSyncCallback(dropAVFrame);
packets.setSyncCallback(dropAVPacket);
}
VideoChannel::~VideoChannel() {
DELETE(audioChannel);
}
void *task_video_decode(void *arg){
auto *videoChannel = static_cast<VideoChannel *> (arg);
videoChannel->video_decode();
return nullptr;
}
void *task_video_play(void *arg){
auto *videoChannel = static_cast<VideoChannel *>(arg);
videoChannel->video_play();
return nullptr;
}
void VideoChannel::video_decode() {
AVPacket *avPacket;
while (isPlaying){
if (isPlaying && frames.size()>100){
av_usleep(10*1000);
continue;
}
// 阻塞式函数,取出刚刚packets队列中的数据
int ret = packets.getQueueAndDel(avPacket);
if (!isPlaying){
// 防止取出数据后,已经关闭播放,
break;
}
if (!ret){
// 没取成功,继续取,有可能是没有数据
continue;
}
// 最新的ffmpeg,和旧版本差别很大,新版本:1.发送packet(压缩包)给缓冲区,2.从缓冲区拿出(原始包)
// ffmpeg源码缓存一分packet,可以释放
ret = avcodec_send_packet(avCodecContext,avPacket);
if (ret){
break;
}
// 从缓冲区获取原始包
AVFrame *avFrame = av_frame_alloc();
ret = avcodec_receive_frame(avCodecContext,avFrame);
if (ret == AVERROR(EAGAIN)){
// B帧 B帧参考前面成功 B帧参考后面失败 可能是P帧没有出来,再拿一次就行了
continue;
} else if (ret != 0){
// 接码失败,马上释放,防止内存泄漏
if (avFrame){
releaseAVFrame(&avFrame);
}
break;
}
frames.insertToQueue(avFrame);
//释放packet及packet成员指向的空间释放,减一释放,当减到为0时,释放成员指向的堆区
av_packet_unref(avPacket);
releaseAVPacket(&avPacket);
}
// 释放
av_packet_unref(avPacket);
releaseAVPacket(&avPacket);
}
// 把队列里面的原始包(AVFrame*)取出来,播放
void VideoChannel::video_play() {
AVFrame *frame = 0;
uint8_t *dst_data[4];//RGBA
int dst_linesize[4];
// 原始包(yuv)----》(libswscale) Android屏幕(RGBA数据)
// 按照图像的宽高,格式分析图形内存
/**
* V . FFMPEG 初始化图像数据存储内存
1 . 图像数据保存 : 需要两个变量来进行存储 , 一个是指针 , 指向一块内存 , 该内存中存储实际的图像数据 , 一个是 int 数值 , 存储该内存中存储了多少数据 ;
① 指针 : 将图像数据保存到 uint8_t *dst_data[4] 指针数组中的指针元素指向的内存中 ;
② 行数 : int dst_linesize[4] 中存储其行数 , 代表了上面指针指向的内存每行存储了多少数据 ;
*/
/**
* 根据图像的宽高 , 像素格式 , 为 相应的 指向图像数据的指针 和 行数 进行初始化
* 参数一:保存图形通道的地址;参数二:保存图像每个通道的内存对齐的步长,即一行的对齐内存宽度,此值大小等于图像宽度
* 参数五:要申请内存的图像的像素格式;参数六:用于内存对齐的值
*/
av_image_alloc(dst_data,dst_linesize,avCodecContext->width,avCodecContext->height,AV_PIX_FMT_RGBA,1);
//yuv -> rgba
SwsContext *swsContext = sws_getContext(
//输入
avCodecContext->width,
avCodecContext->height,
avCodecContext->pix_fmt,//自动获取像素给谁 AV_PIX_FMT_YUV420P
//输出
avCodecContext->width,
avCodecContext->height,
AV_PIX_FMT_RGBA,
SWS_BILINEAR,NULL,NULL,NULL);
while (isPlaying){
int ret = frames.getQueueAndDel(frame);
if (!isPlaying){
break;
}
if (!ret){
continue;
}
// 格式转换 yuv ---> rgba
sws_scale(swsContext,frame->data,frame->linesize,
0,avCodecContext->height,
//输出
dst_data,dst_linesize);
// 延时
double extra_delay = frame->repeat_pict/(2*fps);
double fps_delay = 1.0 / fps;
double real_delay = fps_delay + extra_delay;//当前帧的延时时间
// 下面是音频同步
//基础:数组被传递会退化成指针,默认就是去1元素
renderCallback(dst_data[0],avCodecContext->width,avCodecContext->height,dst_linesize[0]);
av_frame_unref(frame);
releaseAVFrame(&frame);
}
av_frame_unref(frame); // 减1 = 0 释放成员指向的堆区
releaseAVFrame(&frame); // 释放AVFrame * 本身的堆区空间
isPlaying =0;
av_free(&dst_data[0]);
sws_freeContext(swsContext); // free(sws_ctx); FFmpeg必须使用人家的函数释放,直接崩溃
}
void VideoChannel::start() {
isPlaying = true;
// 启动队列
packets.setWork(1);
frames.setWork(1);
// 第一个线程:视频:取出队列的压缩包,进行解码,解码后的原始包在push到第二队列中取(视频yuv)
pthread_create(&pid_video_decode, nullptr,task_video_decode,this);
// 第二线程:从队列中取出原始包,播放
pthread_create(&pid_video_play, nullptr,task_video_play,this);
}
void VideoChannel::stop() {
}
?然后用函数指针回调把数据push到缓冲区,让其绘制;
native-lib.cpp代码:
#include <jni.h>
#include <string>
#include <android/log.h>
#include <android/native_window_jni.h> // ANativeWindow 用来渲染画面的 == Surface对象
#include "Player.h"
#include "JINCallbackHelper.h"
#include <queue>
#include <pthread.h>
#define TAG "ManiActivity"
JavaVM *vm = 0;
ANativeWindow *window = 0;
Player *player = nullptr;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; // 静态初始化 锁
jint JNI_OnLoad(JavaVM * vm, void * args) {
::vm = vm;
return JNI_VERSION_1_6;
}
// 实现渲染
void renderFrame(uint8_t * src_data, int width, int height, int src_lineSize){
pthread_mutex_lock(&mutex);
if(!window){
pthread_mutex_unlock(&mutex);
}
ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);
ANativeWindow_Buffer window_buffer;
//如果在我渲染的时候,是被锁住的,那我就无法渲染,我需要释放,防止出现死锁
if(ANativeWindow_lock(window,&window_buffer,0)){
ANativeWindow_release(window);
window = 0;
//解锁,怕出现死锁
pthread_mutex_unlock(&mutex);
return;
}
uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);
// 一个像素是4个字节rgba
int dst_linesize = window_buffer.stride * 4;
for(int i = 0;i<window_buffer.height;++i){
// 这里是首地址,通过首地址指针位移得到指针指向的数据
memcpy(dst_data + i*dst_linesize,src_data + i * src_lineSize,dst_linesize);
}
// 解锁后并且刷新window_buffer的数据显示画面
ANativeWindow_unlockAndPost(window);
pthread_mutex_unlock(&mutex);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_game_demondk_DerryPlayer_prepareNative(JNIEnv *env, jobject job, jstring data_source) {
const char * source = env->GetStringUTFChars(data_source,0);
auto *hepler = new JINCallbackHelper(vm,env,job);
player = new Player(env, source, hepler);
player->setRenderCallback(renderFrame);
player->perpare();
env->ReleaseStringUTFChars(data_source,source);
}
extern "C"
JNIEXPORT void JNICALL
Java_com_game_demondk_DerryPlayer_startNative(JNIEnv *env, jobject thiz) {
if (player){
player->start();
}
}
extern "C"
JNIEXPORT void JNICALL
Java_com_game_demondk_DerryPlayer_stopNative(JNIEnv *env, jobject thiz) {
// TODO: implement stopNative()
}
extern "C"
JNIEXPORT void JNICALL
Java_com_game_demondk_DerryPlayer_releaseNative(JNIEnv *env, jobject thiz) {
// TODO: implement releaseNative()
}
extern "C"
JNIEXPORT void JNICALL
Java_com_game_demondk_DerryPlayer_setSurfaceNative(JNIEnv *env, jobject thiz, jobject surface) {
pthread_mutex_lock(&mutex);
if (window){
ANativeWindow_release(window);
window = nullptr;
}
//创建新的窗口,用于视频显示
window = ANativeWindow_fromSurface(env,surface);
LOGI("Tag========surface");
pthread_mutex_unlock(&mutex);
}
?FFMPEG 初始化图像数据存储内存及相应函数意义
1 . 图像数据保存 : 需要两个变量来进行存储 , 一个是指针 , 指向一块内存 , 该内存中存储实际的图像数据 , 一个是 int 数值 , 存储该内存中存储了多少数据 ;
① 指针 : 将图像数据保存到 uint8_t *dst_data[4] 指针数组中的指针元素指向的内存中 ;
② 行数 : int dst_linesize[4] 中存储其行数 , 代表了上面指针指向的内存每行存储了多少数据 ;
1 . 获取转换上下文
SwsContext *swsContext = sws_getContext(
//源图像的 宽 , 高 , 图像像素格式
avCodecContext->width, avCodecContext->height, avCodecContext->pix_fmt,
//目标图像 大小不变 , 不进行缩放操作 , 只将像素格式设置成 RGBA 格式的
avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA,
//使用的转换算法 , FFMPEG 提供了许多转换算法 , 有快速的 , 有高质量的 , 需要自己测试
SWS_BILINEAR,
//源图像滤镜 , 这里传 NULL 即可
0,
//目标图像滤镜 , 这里传 NULL 即可
0,
//额外参数 , 这里传 NULL 即可
0
);
2 . 初始化图像存储内存
//指针数组 , 数组中存放的是指针
uint8_t *dst_data[4];
//普通的 int 数组
int dst_linesize[4];
//初始化 dst_data 和 dst_linesize , 为其申请内存 , 注意使用完毕后需要释放内存
av_image_alloc(dst_data, dst_linesize,
avCodecContext->width, avCodecContext->height, AV_PIX_FMT_RGBA,
1);
3 . 格式转换
sws_scale(
//SwsContext *swsContext 转换上下文
swsContext,
//要转换的数据内容
avFrame->data,
//数据中每行的字节长度
avFrame->linesize,
0,
avFrame->height,
//转换后目标图像数据存放在这里
dst_data,
//转换后的目标图像行数
dst_linesize
);
不明白的几点:
1.uint8_t *dst_data[4];//RGBA
int dst_linesize[4];
dst_data 每一个对应RGBA的首地址,接下来可以根据指针的位置得到数据,dst_linesize 是每行的大小;
2.数据是一行一行的复制到ANativeWindow缓冲区,通过ANativeWindow_unlockAndPost() 去刷新之后才去绘制;
3.计算机字节对齐的原理
各个硬件平台对存储空间的处理上有很大的不同。一些平台对某些特定类型的数据只能从某些特定地址开始存取。 比如有些架构的CPU在访问 一个没有进行对齐的变量的时候会发生错误,那么在这种架构下编程必须保证字节对 齐.其他平台可能没有这种情况,但是最常见的是如果不按照适合其平台要求对 数据存放进行对齐,会在存取效率 上带来损失。比如有些平台每次读都是从偶地址开始,如果一个int型(假设为32位系统)如果存放在偶地址开始 的地方,那么一个读周期就可以读出这32bit,而如果存放在奇地址开始的地方,就需要2个读周期,并对两次读出 的结果的高低字节进行拼凑才能得到该32bit数据。
还有就是注意内存泄露,该释放的释放,保证队列大小,不固定大小数据就会一股脑放进去;
|