NDK25_FFmpeg视频解码与原生绘制

NDK开发汇总

一 视频相关基础

View

SurfaceView

Activity的View hierachy的树形结构,最顶层的DecorView,也就是根结点视图,在SurfaceFlinger中有对应的Layer。

对于具有SurfaceView的窗口来说,每一个SurfaceView在SurfaceFlinger服务中还对应有一个独立的Layer,用来单独描述它的绘图表面,以区别于它的宿主窗口的绘图表面。

在WMS和SurfaceFlinger中,它与宿主窗口是分离的。这样的好处是对这个Surface的渲染可以放到单独线程去做。这对于一些游戏、视频等性能相关的应用非常有益,因为它不会影响主线程对事件的响应。但它也有缺点,因为这个Surface不在View hierachy中,它的显示也不受View的属性控制,所以不能进行平移,缩放等变换,一些View中的特性也无法使用。

在这里插入图片描述

优点:

  • 可以在一个独立的线程中进行绘制,不会影响主线程。
  • 使用双缓冲机制,播放视频时画面更流畅。

缺点:

  • Surface不在View hierachy中,显示也不受View的属性控制,所以不能进行平移,缩放等变换。

双缓冲:两张Canvas,一张frontCanvas和一张backCanvas,每次实际显示的是frontCanvas,backCanvas存储的是上一次更改前的视图,当使用lockCanvas()获取画布,得到的backCanvas而不是正在显示的frontCanvas,之后在获取到的backCanvas上绘制新视图,再unlockCanvasAndPost更新视图,上传的这张canvas将替换原来的frontCanvas作为新的frontCanvas,原来的frontCanvas将切换到后台作为backCanvas。

TextureView

在4.0(API level 14)中引入。和SurfaceView不同,不会在WMS中单独创建窗口,而是作为View hierachy中的一个普通View,因此可以和其它普通View一样进行移动,旋转,缩放,动画等变化。TextureView必须在硬件加速的窗口中。

优点:

  • 支持移动、旋转、缩放等动画,支持截图

缺点:

  • 必须在硬件加速的窗口中使用,占用内存比SurfaceView高(因为开启了硬件加速),可能有1〜3帧延迟。

Surface与SurfaceTexture

如果说Surface是画布(画框), SurfaceTexture则是一幅画。可以使用new Surface(SurfaceTexture)创建一个Surface。SurfaceTexture并不直接显示图像,而是转为一个外部纹理(图像),用于图像的二次处理。

//创建一个纹理id
int[] mTextures = new int[1];
SurfaceTexture mSurfaceTexture = new SurfaceTexture(mTextures[0]);
//摄像头作为图像流 交给SurfaceTexture处理
Camera.setPreviewTexture(mSurfaceTexture);
//OpengGL可以通过 mTextures 对摄像头图像进行二次处理

码率帧率

视频码率就是数据传输时单位时间传送的数据位数,一般我们用的单位是kbps即千位每秒。通俗一点的理解就是取样率,单位时间内取样率越大,精度就越高,处理出来的文件就越接近原始文件。码率和质量成正比,但是文件体积也和码率成正比,即码率越高越清晰,视频文件越大。码率率超过一定数值,对图像的质量没有多大影响,因为原始图像只有那么清晰,再高码率也不会变的比原图更清晰

每秒的帧数表示图形处理器处理场时每秒钟能够更新的次数,一般使用fps(Frames per Second)表示。因此帧率越高,画面越流畅。比如Android理论上是16ms一张图像,即60fps。

ANativeWindow

ANativeWindow代表的是本地窗口,可以看成NDK提供Native版本的Surface。通过ANativeWindow_fromSurface获得ANativeWindow指针,ANativeWindow_release进行释放。类似Java,可以对它进行lock、unlockAndPost以及通过ANativeWindow_Buffer进行图像数据的修改。

#include <android/native_window_jni.h>
//先释放之前的显示窗口
if (window) {
	ANativeWindow_release(window);
	window = 0;
}
//创建新的窗口用于视频显示
window = ANativeWindow_fromSurface(env, surface);
//设置窗口属性
ANativeWindow_setBuffersGeometry(window, w,
                                     h,
                                     WINDOW_FORMAT_RGBA_8888);

ANativeWindow_Buffer window_buffer;
if (ANativeWindow_lock(window, &window_buffer, 0)) {
	ANativeWindow_release(window);
	window = 0;
	return;
}
//填充rgb数据给dst_data
uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);
//......
ANativeWindow_unlockAndPost(window);

在NDK中使用ANativeWindow编译时需要链接NDK中的libandroid.so

#编译链接NDK/platforms/android-X/usr/lib/libandroid.so
target_link_libraries(XXX android )

二 视频解码

接着上一篇内容:NDK24_FFmpeg集成及初始化
解码完成并回调通知java层后,java层就需要进行音视频的播放,本文主要讲述视频播放

调用顺序

MainActivity$DNPlayer.OnPrepareListener:onPrepare
->dnPlayer.start(); -> native_start()
->native-lib:Java_com_cn_ray_player_DNPlayer_native_1start
->ffmpeg:start();  ->_start()
->videoChannel:play()->decode_task()、render_task()
->videoChannel:decode() 、render()
->render回调 ->native-lib: render()

1 设置Surface

DNPlayer

public class DNPlayer implements SurfaceHolder.Callback {
    
    
	...
    @Override
    public void surfaceChanged(SurfaceHolder surfaceHolder, int i, int i1, int i2) {
    
    
       native_setSurface(surfaceHolder.getSurface()) ;
    }
	...

	 native void native_setSurface(Surface suface);
}

native-lib

ANativeWindow *window = 0;

extern "C"
JNIEXPORT void JNICALL
Java_com_cn_ray_player_DNPlayer_native_1setSurface(JNIEnv *env, jobject instance, jobject surface) {
    
    
    pthread_mutex_lock(&mutex);
    if (window) {
    
    
        //把老的释放
        ANativeWindow_release(window);
        window = 0;
    }
    window = ANativeWindow_fromSurface(env, surface);
    pthread_mutex_unlock(&mutex);
}

2 创建音视频基类BaseChannel

//
// Created by PF0ZYBAJ on 2020-9-10.
//

#ifndef PLAYER_BASECHANNEL_H
#define PLAYER_BASECHANNEL_H

extern "C" {
    
    
#include <libavcodec/avcodec.h>
};

#include "safe_queue.h"

class BaseChannel {
    
    
public:
    BaseChannel(int id,AVCodecContext *avCodecContext) : id(id),avCodecContext(avCodecContext) {
    
    }

    //子类要继承 virtual
    virtual ~BaseChannel() {
    
    
        packets.setReleaseCallback(BaseChannel::releaseAvPacket);
        packets.clear();
    }


    /**
     * 释放 AVPacket
     * @param packet
     */
    static void releaseAvPacket(AVPacket** packet) {
    
    
        if (packet) {
    
    
            av_packet_free(packet);
            //为什么用指针的指针?
            // 指针的指针能够修改传递进来的指针的指向
            *packet = 0;
        }
    }

    static void releaseAvFrame(AVFrame** frame){
    
    
        if (frame) {
    
    
            av_frame_free(frame);
            //为什么用指针的指针?
            // 指针的指针能够修改传递进来的指针的指向
            *frame = 0;
        }
    }
    //纯虚方法 相当于抽象方法
    virtual void play() = 0;

    int id;
    SafeQueue<AVPacket *> packets;
    bool isPlaying;
    AVCodecContext *avCodecContext;
};


#endif //PLAYER_BASECHANNEL_H

3 VideoChannel 实现视频播放逻辑

进行decode 和render

将视频的解编码和渲染各开一个线程,用SafeQueue存储数据,避免渲染阻塞解编码
VideoChannel.h

//
// Created by Administrator on 2018/9/5.
//

#ifndef PLAYER_VIDEOCHANNEL_H
#define PLAYER_VIDEOCHANNEL_H


#include "BaseChannel.h"

extern "C" {
    
    
#include <libswscale/swscale.h>
};

/**
 * 1、解码
 * 2、播放
 */
typedef void (*RenderFrameCallback)(uint8_t *,int,int,int);
class VideoChannel : public BaseChannel {
    
    
public:
    VideoChannel(int id, AVCodecContext *avCodecContext);

    ~VideoChannel();

    //解码+播放
    void play();

    void decode();

    void render();

    void setRenderFrameCallback(RenderFrameCallback callback);
private:
    pthread_t pid_decode;
    pthread_t pid_render;
    SafeQueue<AVFrame *> frames;
    SwsContext *swsContext=0;
    RenderFrameCallback callback;
};


#endif //PLAYER_VIDEOCHANNEL_H

VideoChannel.cpp

//
// Created by Administrator on 2018/9/5.
//

extern "C"{
    
    
#include <libavutil/imgutils.h>
}
#include "VideoChannel.h"
#include "macro.h"

void *decode_task(void *args) {
    
    
    VideoChannel *channel = static_cast<VideoChannel *>(args);
    channel->decode();
    return 0;
}

void *render_task(void *args) {
    
    
    VideoChannel *channel = static_cast<VideoChannel *>(args);
    channel->render();
    return 0;
}



VideoChannel::VideoChannel(int id, AVCodecContext *avCodecContext) : BaseChannel(id,
                                                                                 avCodecContext) {
    
    

    frames.setReleaseCallback(releaseAvFrame);
}

VideoChannel::~VideoChannel() {
    
    
    frames.clear();
}

void VideoChannel::play() {
    
    
    isPlaying = 1;
    //1、解码
    pthread_create(&pid_decode, 0, decode_task, this);
    //2、播放
    pthread_create(&pid_render, 0, render_task, this);
}

//解码
void VideoChannel::decode() {
    
    
    AVPacket *packet = 0;
    while (isPlaying) {
    
    
        //取出一个数据包
        int ret = packets.pop(packet);
        LOGE("decode 取出数据包 %d",ret);
        if (!isPlaying) {
    
    
            break;
        }
        //取出失败
        if (!ret) {
    
    
            continue;
        }
        //把包丢给解码器
        ret = avcodec_send_packet(avCodecContext, packet);
        LOGE("decode 解码 %d",ret);
        releaseAvPacket(&packet);
        if(ret == AVERROR_INVALIDDATA){
    
    
            continue;
            LOGE("decode 解码 AVERROR_INVALIDDATA %d",ret);
        }
        //重试
        if (ret != 0) {
    
    
            break;
        }
        //代表了一个图像 (将这个图像先输出来)
        AVFrame *frame = av_frame_alloc();
        //从解码器中读取 解码后的数据包 AVFrame
        ret = avcodec_receive_frame(avCodecContext, frame);
        //需要更多的数据才能够进行解码
        if (ret == AVERROR(EAGAIN)) {
    
    
            continue;
        } else if(ret != 0){
    
    
            break;
        }
        //再开一个线程 来播放 (流畅度)
        LOGE("decode push");
        frames.push(frame);
    }
    LOGE("decode 结束");
    releaseAvPacket(&packet);
}

//播放
void VideoChannel::render() {
    
    

    //目标: RGBA
    swsContext = sws_getContext(
            avCodecContext->width, avCodecContext->height,avCodecContext->pix_fmt,
            avCodecContext->width, avCodecContext->height,AV_PIX_FMT_RGBA,
            SWS_BILINEAR,0,0,0);
    AVFrame* frame = 0;
    //指针数组
    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);
    while (isPlaying){
    
    
        int ret = frames.pop(frame);
        if (!isPlaying){
    
    
            break;
        }
        //src_linesize: 表示每一行存放的 字节长度
        sws_scale(swsContext, reinterpret_cast<const uint8_t *const *>(frame->data),
                  frame->linesize, 0,
                  avCodecContext->height,
                  dst_data,
                  dst_linesize);
        //回调出去进行播放
        callback(dst_data[0],dst_linesize[0],avCodecContext->width, avCodecContext->height);
        releaseAvFrame(&frame);
    }
    av_freep(&dst_data[0]);
    releaseAvFrame(&frame);
}

void VideoChannel::setRenderFrameCallback(RenderFrameCallback callback) {
    
    
    this->callback = callback;
}

4 启动解码

java层收到回调通知启动

 dnPlayer.setOnPrepareListener(new DNPlayer.OnPrepareListener() {
    
    
            @Override
            public void onPrepare() {
    
    
                dnPlayer.start();
            }
        });

//DNPlayer.java 中:
native void native_start();

FFmpeg实现启动逻辑
native-lib

extern "C"
JNIEXPORT void JNICALL
Java_com_cn_ray_player_DNPlayer_native_1start(JNIEnv *env, jobject instance) {
    
    
    ffmpeg->start();
}

FFmpeg.cpp

void *play(void *args) {
    
    
    DNFFmpeg *ffmpeg = static_cast<DNFFmpeg *>(args);
    ffmpeg->_start();
    return 0;
}

void DNFFmpeg::start() {
    
    
    // 正在播放
    isPlaying = 1;
    if (videoChannel){
    
    
        //设置为工作状态
        videoChannel->packets.setWork(1);
        videoChannel->play();
    }
    pthread_create(&pid_play, 0, play, this);
}


/**
 * 专门读取数据包
 */
void DNFFmpeg::_start() {
    
    
    //1、读取媒体数据包(音视频数据包)
    int ret;
    while (isPlaying) {
    
    
        AVPacket *packet = av_packet_alloc();
        ret = av_read_frame(formatContext, packet);
        //=0成功 其他:失败
        if (ret == 0) {
    
    
            //stream_index 这一个流的一个序号
            if (audioChannel && packet->stream_index == audioChannel->id) {
    
    

            } else if (videoChannel && packet->stream_index == videoChannel->id) {
    
    
                videoChannel->packets.push(packet);
            }
        } else if (ret == AVERROR_EOF) {
    
    
            //读取完成 但是可能还没播放完

        } else {
    
    
            //
        }

    }

};

5 原生绘制

定义绘制方法

native-lib

//画画
void render(uint8_t *data, int lineszie, int w, int h) {
    
    
    pthread_mutex_lock(&mutex);
    if (!window) {
    
    
        pthread_mutex_unlock(&mutex);
        return;
    }
    //设置窗口属性
    ANativeWindow_setBuffersGeometry(window, w,
                                     h,
                                     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;
    }
    //填充rgb数据给dst_data
    uint8_t *dst_data = static_cast<uint8_t *>(window_buffer.bits);
    // stride:一行多少个数据(RGBA) *4
    int dst_linesize = window_buffer.stride * 4;
    //一行一行的拷贝
    for (int i = 0; i < window_buffer.height; ++i) {
    
    
        //memcpy(dst_data , data, dst_linesize);
        memcpy(dst_data + i * dst_linesize, data + i * lineszie, dst_linesize);
    }
    ANativeWindow_unlockAndPost(window);
    pthread_mutex_unlock(&mutex);
}

设置回调

native-lib 中: ffmpeg->setRenderFrameCallback(render);

extern "C"
JNIEXPORT void JNICALL
Java_com_cn_ray_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->setRenderFrameCallback(render);
    ffmpeg->prepare();
    env->ReleaseStringUTFChars(dataSource_, dataSource);
}

DNFFpage中

void DNFFmpeg::setRenderFrameCallback(RenderFrameCallback callback){
    
    
    this->callback = callback;
}

void DNFFmpeg::_prepare() {
    
    
	...
    videoChannel = new VideoChannel(stream->index,context);
    ...
}




猜你喜欢

转载自blog.csdn.net/baopengjian/article/details/108571736
今日推荐