文章目录
一 视频相关基础
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);
...
}