Android 音视频FFmpeg (二)

这一步是先做视频播放

首先在页面加入

<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数据。

还有就是注意内存泄露,该释放的释放,保证队列大小,不固定大小数据就会一股脑放进去;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值