NDK FFmpeg音视频播放器二

news/2024/7/10 22:20:18 标签: ffmpeg, 音视频, NDK

NDK前期基础知识终于学完了,现在开始进入项目实战学习,通过FFmpeg实现一个简单的音视频播放器。

本文主要内容如下:

  1. 阻塞式队列SafeQueue。

  1. 音视频BaseChannel基础通道。

  1. 音视频压缩包加入队列。

  1. 视频解码与播放。

  1. ANativeWindow渲染

用到的ffmpeg、rtmp等库资源:

https://wwgl.lanzout.com/iN21C0qiiija

音视频播放流程:

1.准备工作完成,音视频解封装后,

通过音视频媒体上下文AVFormatContext获取到具体的音视频压缩包AVPacket

2.将音视频压缩包AVPacket解压,得到音视频原始包AVFrame(可播放的文件包)

3.拿到音视频原始包AVFrame,进行播放。

代码逻辑:

1.获取压缩包AVPacket、获取原始包AVFrame、播放;是个生产消费,重复并发进行的过程,可以通过队列queue来完成。

2.创建两个队列queue,压缩包AVPacket队列和原始包AVFrame队列;

3.创建循环获取压缩包AVPacket,并push压缩包到AVPacket队列;

4.创建循环去AVPacket队列中获取压缩包AVPacket,解压得到原始包AVFrame,并push原始包到AVFrame队列;

5.创建循环去AVFrame队列中获取原始包AVFrame,进行播放;

6.音频和视频都有相同的解压、原始包、播放动作,故创建分别创建音频和视频队列,并封装到音频AudioChannel通道和视频VideoChannel通道中去处理;音频AudioChannel通道和视频VideoChannel通道,重复部分封装BaseChannel通道去。

一、阻塞式队列SafeQueue

封装线程安全队列SafeQueue,通过pthread_mutex_t互斥锁和pthread_cond_t条件变量来实现数据入队,出队,等待和唤醒工作。

#ifndef NDKPLAYER_SAFEQUEUE_H
#define NDKPLAYER_SAFEQUEUE_H

#include <queue>
#include <pthread.h>

using namespace std;

/**
 * 线程安全队列
 * @tparam T  泛型:存放任意类型
 */
template<typename T>
class SafeQueue {
private:
    typedef void (*ReleaseCallback)(T *); // 函数指针定义 做回调 用来释放T里面的内容的
private:
    queue<T> queue;
    pthread_mutex_t mutex; // 互斥锁 安全
    pthread_cond_t cond; // 等待 和 唤醒
    int work; // 标记队列是否工作
    ReleaseCallback releaseCallback;
public:
    SafeQueue() {
        pthread_mutex_init(&mutex, 0); // 初始化互斥锁
        pthread_cond_init(&cond, 0); // 初始化条件变量
    }

    virtual ~SafeQueue() {
        pthread_mutex_destroy(&mutex); // 释放互斥锁
        pthread_cond_destroy(&cond); // 释放条件变量
    }

    /**
     * 入队 [ AVPacket *  压缩包]  [ AVFrame * 原始包]
     */
    void insertToQueue(T value) {
        pthread_mutex_lock(&mutex); // 多线程的访问(先锁住)
        if (work) {
            // 工作状态,入队
            queue.push(value);
            // 当插入数据包 进队列后,发出通知唤醒
            pthread_cond_signal(&cond);
        } else {
            //非工作状态,释放value
            if (releaseCallback) {
                releaseCallback(&value);
            }
        }
        pthread_mutex_unlock(&mutex); // 多线程的访问(要解锁)
    }

    /**
     *  出队 [ AVPacket *  压缩包]  [ AVFrame * 原始包]
     */
    int getQueueAndDel(T &value) {
        int result = 0;
        pthread_mutex_lock(&mutex); // 多线程的访问(先锁住)
        while (work && queue.empty()) {
            // 如果是工作 并且 队列里面没有数据,就阻塞在这里
            pthread_cond_wait(&cond, &mutex);
        }
        if (!queue.empty()) {
            // 取出队列的数据包 给外界,并删除队列数据包
            value = queue.front();
            // 删除队列中的数据
            queue.pop();
            // 成功 return true
            result = 1;
        }
        pthread_mutex_unlock(&mutex); // 多线程的访问(要解锁)

        return result;
    }

    /**
     * 设置工作状态,设置队列是否工作
     * @param work
     */
    void setWork(int work) {
        pthread_mutex_lock(&mutex); // 多线程的访问(先锁住)
        this->work = work;
        // 每次设置状态后,就去唤醒
        pthread_cond_signal(&cond);
        pthread_mutex_unlock(&mutex); // 多线程的访问(要解锁)
    }

    int empty() {
        return queue.empty();
    }

    int size() {
        return queue.size();
    }

    /**
     * 清空队列中所有的数据,循环一个一个的删除
     */
    void clear() {
        pthread_mutex_lock(&mutex); // 多线程的访问(先锁住)
        unsigned int size = queue.size();
        for (int i = 0; i < size; ++i) {
            //循环释放队列中的数据
            T value = queue.front();
            if (releaseCallback) {
                releaseCallback(&value); // 让外界去释放堆区空间
            }
            queue.pop(); // 删除队列中的数据,让队列为0
        }
        pthread_mutex_unlock(&mutex); // 多线程的访问(要解锁)
    }

    /**
     * 设置此函数指针的回调,让外界去释放
     * @param releaseCallback
     */
    void setReleaseCallback(ReleaseCallback releaseCallback) {
        this->releaseCallback = releaseCallback;
    }
};

#endif //NDKPLAYER_SAFEQUEUE_H

二、音视频BaseChannel基础通道

BaseChannel封装压缩包和原始包队列

#ifndef NDKPLAYER_BASECHANNEL_H
#define NDKPLAYER_BASECHANNEL_H

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

#include "SafeQueue.h"
#include <android/log.h>

// log宏
#define TAG "NDK"
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)

class BaseChannel {

public:
    int stream_index; // 音频 或 视频 的下标
    SafeQueue<AVPacket *> packets; // 压缩的 数据包
    SafeQueue<AVFrame *> frames; // 原始的 数据包
    bool isPlaying; // 音频 和 视频 都会有的标记 是否播放
    AVCodecContext *codecContext = 0; // 音频 视频 都需要的 解码器上下文

    BaseChannel(int streamIndex, AVCodecContext *codecContext)
            : stream_index(streamIndex), codecContext(codecContext) {
        packets.setReleaseCallback(releaseAVPacket);
        frames.setReleaseCallback(releaseAVFrame);
    }

    // 父类析构一定要加virtual
    virtual ~BaseChannel() {
        // 清空队列
        packets.clear();
        frames.clear();
    }

    /**
     * 释放 队列中 所有的 AVPacket *
     * typedef void (*ReleaseCallback)(T *);
     */
    static void releaseAVPacket(AVPacket **pPacket) {
        if (pPacket) {
            // 释放队列里面的 T == AVPacket
            av_packet_free(pPacket);
            *pPacket = 0;
        }
    }

    /**
     * 释放 队列中 所有的 AVFrame *
     * typedef void (*ReleaseCallback)(T *);
     */
    static void releaseAVFrame(AVFrame **pFrame) {
        if (pFrame) {
            // 释放队列里面的 T == AVFrame
            av_frame_free(pFrame);
            *pFrame = 0;
        }
    }
};


#endif //NDKPLAYER_BASECHANNEL_H

三、音视频压缩包加入队列

创建子线程,把音频和视频 压缩包 加入队列里面去

/**
 * 函数指针
 * 此函数和NdkPlayer这个对象没有关系,你没法拿NdkPlayer的私有成员
 * @return
 */
void *task_start(void *ndk_player) {
    NdkPlayer *ndk_player_ = static_cast<NdkPlayer *>(ndk_player);
    ndk_player_->start_();
    return 0; // 必须返回,否则报错
}

void NdkPlayer::start() {
    // 开始播放
    isPlaying = 1;
    // 音视频通道开始
    if (audio_channel) {
        audio_channel->start();
    }
    if (video_channel) {
        video_channel->start();
    }
    // 创建子线程,把音频和视频 压缩包 加入队列里面去
    pthread_create(&pid_start, 0, task_start, this);
}

/**
 * 循环获取压缩包AVPacket,并push压缩包到队列
 */
void NdkPlayer::start_() {
    LOGI("NdkPlayer::start_()");
    while (isPlaying) {
        // AVPacket 可能是音频 也可能是视频(压缩包)
        AVPacket *packet = av_packet_alloc();
        int result = av_read_frame(format_context, packet);
        // @return 0 if OK
        if (!result) {
            // 把压缩包AVPacket 分别加入音频 和 视频队列
            if (audio_channel && audio_channel->stream_index == packet->stream_index) {
                // 音频
                audio_channel->packets.insertToQueue(packet);
            } else if (video_channel && video_channel->stream_index == packet->stream_index) {
                // 视频
                video_channel->packets.insertToQueue(packet);
            }
        } else if (result == AVERROR_EOF) {
            // end of file == 读到文件末尾了 == AVERROR_EOF
            // 表示读完了,要考虑释放播放完成,并不代表播放完毕
            isPlaying = 0;
            LOGI("NdkPlayer::start_() end");
        } else {
            // av_read_frame 出现了错误,结束当前循环
            break;
        }
    } // end while
    isPlaying = 0;
    audio_channel->stop();
    video_channel->stop();
}

四、视频解码与播放

第一个线程: 视频:取出队列的压缩包 进行编码 编码后的原始包 再push队列中去;

第二线线程:视频:从队列取出原始包,播放

#include "VideoChannel.h"

VideoChannel::VideoChannel(int streamIndex, AVCodecContext *codecContext)
        : BaseChannel(streamIndex, codecContext) {

}

VideoChannel::~VideoChannel() {

}

void VideoChannel::stop() {

}

/**
 * 函数指针 解码
 * @param video_channel
 * @return
 */
void *task_video_decode(void *video_channel) {
    VideoChannel *video_channel_ = static_cast<VideoChannel *>(video_channel);
    video_channel_->video_decode();
    return 0;
}

/**
 * 函数指针 播放
 * @param video_channel
 * @return
 */
void *task_video_play(void *video_channel) {
    VideoChannel *video_channel_ = static_cast<VideoChannel *>(video_channel);
    video_channel_->video_play();
    return 0;
}

void VideoChannel::start() {
    LOGI("VideoChannel::start()");
    isPlaying = 1;
    // 队列开始工作了
    packets.setWork(1);
    frames.setWork(1);
    // 第一个线程: 视频:取出队列的压缩包 进行编码 编码后的原始包 再push队列中去
    pthread_create(&pid_video_decode, 0, task_video_decode, this);
    // 第二线线程:视频:从队列取出原始包,播放
    pthread_create(&pid_video_play, 0, task_video_play, this);
}

/**
 * 第一个线程: 视频:取出队列的压缩包 进行编码 编码后的原始包 再push队列中去
 */
void VideoChannel::video_decode() {
    LOGI("VideoChannel::video_decode()");
    AVPacket *pkt = 0;
    while (isPlaying) {
        // 获取AVPacket *  压缩包
        int result = packets.getQueueAndDel(pkt);
        if (!isPlaying) {
            // 获取压缩包是耗时操作,获取完,如果关闭了播放,跳出循环
            break;
        }
        if (!result) {
            // 获取失败,可能是压缩包数据还没有加入队列,继续获取
            continue;
        }
        // 1.发送pkt(压缩包)给缓冲区,@return 0 on success
        result = avcodec_send_packet(codecContext, pkt);
        // FFmpeg源码缓存一份pkt,释放即可
        releaseAVPacket(&pkt);
        if (result) {
            // avcodec_send_packet 出现了错误
            break;
        }
        AVFrame *frame = av_frame_alloc();
        // 2.从缓冲区拿出来(原始包),@return 0: success
        result = avcodec_receive_frame(codecContext, frame);
        if (result == AVERROR(EAGAIN)) {
            // B帧  B帧参考前面成功  B帧参考后面失败   可能是P帧没有出来,再拿一次就行了
            continue;
        } else if (result != 0) {
            // avcodec_receive_frame 出现了错误
            break;
        }
        // 拿到了原始包,并将原始包push到队列
        frames.insertToQueue(frame);
    }
    // 解码获取原始包后,释放压缩包
    releaseAVPacket(&pkt);
}

/**
 * 第二线线程:视频:从队列取出原始包,播放
 */
void VideoChannel::video_play() {
    LOGI("VideoChannel::video_play()");
    AVFrame *frame = 0;
    uint8_t *dst_data[4]; // RGBA 播放文件
    int dst_linesize[4]; // RGBA
    //给 dst_data 申请内存   width * height * 4 xxxx
    av_image_alloc(dst_data, dst_linesize,
                   codecContext->width, codecContext->height, AV_PIX_FMT_RGBA, 1);
    // SWS_BILINEAR 适中算法
    SwsContext *sws_ctx = sws_getContext(
            // 下面是输入环节
            codecContext->width,
            codecContext->height,
            codecContext->pix_fmt, // 自动获取 xxx.mp4 的像素格式  AV_PIX_FMT_YUV420P // 写死的
            // 下面是输出环节
            codecContext->width,
            codecContext->height,
            AV_PIX_FMT_RGBA,
            SWS_BILINEAR, NULL, NULL, NULL);
    while (isPlaying) {
        int result = frames.getQueueAndDel(frame);
        if (!isPlaying) {
            break; // 如果关闭了播放,跳出循环,releaseAVFrame(&frame);
        }
        if (!result) { // ret == 0
            continue; // 哪怕是没有成功,也要继续(假设:你生产太慢(原始包加入队列),我消费就等一下你)
        }
        // 格式转换 yuv ---> rgba
        sws_scale(sws_ctx,
                // 下面是输入环节 YUV的数据
                  frame->data, frame->linesize,
                  0, codecContext->height,

                // 下面是输出环节  成果:RGBA数据 dst_data
                  dst_data,
                  dst_linesize
        );
        /**
         * ANatvieWindows 渲染工作
         * SurfaceView ----- ANatvieWindows
         * 这里拿不到Surface,只能函数指针renderCallback()将RGBA数据 dst_data 回调给 native-lib.cpp,显示
         * 函数指针renderCallback()
         * 参数1:RGBA数据 dst_data 数组被传递会退化成指针,默认就是取第1元素
         * 参数2:视频宽
         * 参数3:视频高
         * 参数4:数据长度
         */
        this->renderCallback(dst_data[0], codecContext->width, codecContext->height,
                             dst_linesize[0]);
        // 释放原始包,因为已经被渲染完了,没用了
        releaseAVFrame(&frame);
    }
    releaseAVFrame(&frame);
    isPlaying = 0;
    av_free(&dst_data[0]);
    // free(sws_ctx); FFmpeg必须使用人家的函数释放,直接崩溃
    sws_freeContext(sws_ctx);
}

void VideoChannel::setRenderCallback(RenderCallback renderCallback) {
    this->renderCallback = renderCallback;
}

五、ANativeWindow渲染

1)初始化surfaceView

private SurfaceView surfaceView;
surfaceView = findViewById(R.id.surfaceView);
mNdkPlayer = new NdkPlayer(dataSource);
mNdkPlayer.setSurfaceHolder(surfaceView);

2)绑定surfaceHolder

public class NdkPlayer implements SurfaceHolder.Callback {
    private SurfaceHolder surfaceHolder;
    public void setSurfaceHolder(SurfaceView surfaceView) {
        if (surfaceHolder != null) {
            // 清除上一次数据
            surfaceHolder.removeCallback(this);
        }
        this.surfaceHolder = surfaceView.getHolder();
        // 添加监听
        surfaceHolder.addCallback(this);
    }

    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {

    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {
        setSurfaceNative(holder.getSurface());
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {

    }

    /**
     * native函数区域
     */
    private native void setSurfaceNative(Surface surface);
}

3)关联Native层ANativeWindow

ANativeWindow *window = 0;
/**
 * 实例化播放window 关联 surfaceView
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_ndk_player_NdkPlayer_setSurfaceNative(JNIEnv *env, jobject thiz, jobject surface) {
    // 线程安全,锁住
    pthread_mutex_lock(&mutex);
    // 先释放之前的显示窗口
    if (window) {
        ANativeWindow_release(window);
        window = 0;
    }
    // 创建新的窗口用于视频显示
    window = ANativeWindow_fromSurface(env, surface);
    pthread_mutex_unlock(&mutex);
}

4)VideoChannel将解析完的RGBA数据(可播放数据)回调给 native-lib.cpp,进行渲染显示。

/**
 * 定义函数指针 实现渲染工作,this->renderCallback()回调到这里来
 */
void renderCallback(uint8_t *dst_data, int width, int height, int dst_linesize) {
    LOGI("native-lib::renderCallback playing");
    pthread_mutex_lock(&mutex);
    // 播放窗口为空,释放锁,小概率出现
    if (!window) {
        pthread_mutex_unlock(&mutex);
        return;
    }
    // 设置窗口的大小,各个属性
    ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);
    // 定义缓冲区 buffer
    ANativeWindow_Buffer window_buffer;
    // 如果在渲染的时候,是被锁住的,那就无法渲染,需要释放,防止出现死锁
    if (ANativeWindow_lock(window, &window_buffer, 0)) {
        ANativeWindow_release(window);
        window = 0;
        pthread_mutex_unlock(&mutex); // 解锁,怕出现死锁
        return;
    }
    // 开始渲染,把rgba数据 ---> 字节对齐 渲染,填充window_buffer画面就出来了
    uint8_t *dst_data_ = static_cast<uint8_t *>(window_buffer.bits);
    // ANativeWindow_Buffer 64字节对齐的数据长度
    int dst_linesize_ = window_buffer.stride * 4;
    for (int i = 0; i < window_buffer.height; ++i) {
        /**
         * 参数1:接收播放数据容器
         * 参数2:RGBA播放数据
         * 参数3:64字节对齐的数据长度
         */
        memcpy(dst_data_ + i * dst_linesize_, dst_data + i * dst_linesize, dst_linesize_);
    }
    // 解锁并且刷新 window_buffer的数据显示画面
    ANativeWindow_unlockAndPost(window);
    pthread_mutex_unlock(&mutex);
}

音视频--视频解码与播放渲染功能完成,接下来。。。


http://www.niftyadmin.cn/n/176229.html

相关文章

免费空间主机是什么?怎么申请免费空间主机

随着网络的普及&#xff0c;越来越多的人开始使用免费空间。这种新的商业模式也让一些商家得以获利。 1&#xff1a;免费空间的概念 免费空间是指允许您自由使用的网络服务。这意味着它可以被任何人用来创建、编辑和发布网站内容或应用程序&#xff0c;而无需考虑任何付费业务协…

离线安装ffmpeg

linux离线安装ffmpeg 获取安装包&#xff1a;[ffmpeg-release](Index of /releases (ffmpeg.org)) 下载最新版本&#xff0c;ffmpeg-4.4.tar.gz 然后传送到服务器上&#xff0c;解压安装 # 解压 tar -zxvf ffmpeg-4.4.tar.gz# 安装 cd ffmpeg-4.4 ./configure --enable-sha…

【人工智能】— CSP约束满足问题、回溯搜索、启发式

【人工智能】— 约束满足问题约束满足问题 CSP示例&#xff1a;地图着色约束图CSP的种类约束类型举例:密码算法现实世界的CSP标准搜索公式回溯搜索改进回溯搜索的效率最少剩余值启发式度启发式最少约束值启发式Forward checking—前向检验Constraint propagation — 约束传播约…

通过小白数据恢复如何才能还原文件资料

移动硬盘其实是我们日常办公生活中很常用的存储工具&#xff0c;包括各类u盘以及内存卡等等&#xff0c;如果操作不当可能会出现数据丢失等各种现象&#xff0c;这时要怎么将移动硬盘数据恢复?我们其实可以考虑使用极速数据恢复app找回&#xff0c;下面小编就给大家介绍下常见…

CDN和CDN加速有什么关联

CDN&#xff0c;很多人可能没见过这个词&#xff0c;它的本义其实就是内容分发网络。它被设计出来主要目的是解决早期互联网数据传输存在拥堵和稳定性差的问题。它不但可以使内容传输的更快、更稳定&#xff0c;还能抵御恶意流量攻击。在原本互联网的基础上利用搭建在全国乃至全…

Python数据结构与算法篇(九)-- 位运算与使用技巧

计算机中的数在内存中都是以二进制形式进行存储的&#xff0c;用位运算就是直接对整数在内存中的二进制位进行操作&#xff0c;因此其执行效率非常高&#xff0c;在程序中尽量使用位运算进行操作&#xff0c;这会大大提高程序的性能。 1 操作符 1.1 基本运算 & 与运算 两…

一种LCD屏闪问题的调试

背景 项目使用ESP32-S3 RGB接口驱动的LCD, 框架 idf-v5.0, LVGL-v7.11 显示画面正常, 但肉眼可见的像是背光在闪烁, 背光电路是应用很久的经典电路, 且排查背光驱动无错, 但开机一段时间后, 闪烁会明显减轻 记录 这块屏的显示驱动芯片为ST7701S, 查看芯片手册有说明特定的上…

使用D435i相机录制TUM格式的数据集

目录前言系统版本一、使用realsense SDK录制bag包的情况1.录制视频2.、提取rgb和depth图片1.2.3.对齐时间戳二、用realsense-ros打开相机录制bag包1.将深度图对齐到RGB2.使用realsense-ros打开相机3.录制rosbag1.直接使用命令2.写一个launch文件4.提取图像以及对齐时间戳前言 …