Android Android FFmpeg视频播放器二 视频封装格式解码播放

Android FFmpeg视频播放器一解封装

视频解封装之后就会得到音频流和视频流,解封状得到的数据是AVPackage类型数据,需要进一步解码成AVFrame一帧一帧数据才能进行播放。

1.从AVPackage队列获取数据进行解码操作

pthread_create(&pid_video_decode, nullptr, task_video_decode, this);

void *task_video_decode(void *args) {
    auto *video_channel = static_cast<VideoChannel *>(args);
    video_channel->video_decode();
    return nullptr;
}

/**
 * 视频解码操作
 * 把队列里面的压缩包(AVPacket *)取出来,然后解码成(AVFrame * )原始包,保存到视频帧队列
 */
void VideoChannel::video_decode() {
    AVPacket *pkt = 0;
    while (isPlaying) {
        if (isPlaying&& frames.size() > AV_MAX_SIZE){
            av_usleep(10*000);
            continue;
        }
        /*阻塞式函数*/
        int ret = packets.getQueueAndDel(pkt); 
        if (!isPlaying) {
            /*如果关闭了播放跳出循环,releaseAVPacket(&pkt);*/
            break; 
        }

        if (!ret) { 
            /* 可能是生产数据慢没拿到数据*/
            continue;
        }

        /*
         * 直接将拿到的 AVPackage* 数据通过avcodec_send_packet 函数丢给ffmpeg缓冲区 解码操作
         * */
        ret = avcodec_send_packet(codecContext, pkt);

        if (ret) {
            break;
        }

        AVFrame *frame = av_frame_alloc();
        /*下面是从 Fmpeg缓冲区 获取原始包AVFrame数据,解码后的视频原始数据包yuv420数据*/
        ret = avcodec_receive_frame(codecContext, frame);
        if (ret == AVERROR(EAGAIN)) {
            continue;
        } else if (ret != 0) {
            if (frame){
                releaseAVFrame(&frame);
            }
            break;
        }
        /*将原始包放到 帧队列 */
        frames.insertToQueue(frame);
        /*使用完记得释放 否则会造成内容泄漏*/
        av_packet_unref(pkt);
        releaseAVPacket(&pkt);
    }
    /*发生异常释放 指针*/
    av_packet_unref(pkt);
    releaseAVPacket(&pkt);
}

  • 由于是耗时操作,所以先pthread_create创建线程
  • 开启循环从AVPackage队列获取AVPackage数据进行解码操作
  • frames 是视频帧队列,队列的阈值是AV_MAX_SIZE,这个值可以自己设置,不要太大,否则在队列保存的数据太大
  • packets.getQueueAndDel(pkt):从队列获取AVPackage类型数据,阻塞队列,如果队列为空会进行阻塞等待。
  • avcodec_send_packet(codecContext, pkt):将获取到的 AVPackage* 数据通过avcodec_send_packet 函数丢给ffmpeg缓冲区 解码操作
  • avcodec_receive_frame:从ffmpeg缓冲区 获取原始包AVFrame数据,原始数据包yuv420数据,获取帧数据后,insertToQueue加入到视频帧队列。

2.从视频帧队列获取数据进行播放

pthread_create(&pid_video_play, nullptr, task_video_play, this);

/*线程回调函数*/
void *task_video_play(void *args) {
    auto *video_channel = static_cast<VideoChannel *>(args);
    video_channel->video_play();
    return nullptr;
}


/**
 * 把队列里面的原始包(AVFrame *)取出来播放
 */
void VideoChannel::video_play() {

    AVFrame *frame = nullptr;
    /*接收RGBA数据*/
    uint8_t *dst_data[4];
    /*接收RGBA 数据长度*/
    int dst_linesize[4];

    /*
     * 原始包(YUV数据) 通过libswscale方法进行数据转换 Android屏幕(RGBA数据)
     * dst_data 申请内存   width * height * 4
     * */
    av_image_alloc(dst_data, dst_linesize,
                   codecContext->width, codecContext->height,
                   AV_PIX_FMT_RGBA, 1);
    /*
     * 初始化转换上下文:SwsContext 
     * 第七个参数:yuv 转rgba flag :SWS_FAST_BILINEAR,快速双线性,速度快可能会模糊,
     * 所以选择SWS_BILINEAR普通双线性就好
     * */
    SwsContext *sws_ctx = sws_getContext(
            /*输入环节*/
            codecContext->width,
            codecContext->height,
            /*自动获取 xxx.mp4 的像素格式 AV_PIX_FMT_YUV420P*/
            codecContext->pix_fmt,
            /*输出环节*/
            codecContext->width,
            codecContext->height,
            AV_PIX_FMT_RGBA,
            SWS_BILINEAR, NULL, NULL, NULL);

    while (isPlaying) {
        int ret = frames.getQueueAndDel(frame);
        if (!isPlaying) {
            /*如果关闭了播放跳出循环,releaseAVPacket(&pkt);*/
            break;
        }
        if (!ret) {
            /* 压缩包加入队列慢,继续*/
            continue;
        }
        /*
         * 将yuv格式数据转换成rgba数据
         * */
        sws_scale(sws_ctx,
                /*输入环节 YUV的数据*/
                  frame->data, frame->linesize,
                  0, codecContext->height,

                /*输出环节:RGBA数据*/
                  dst_data,
                  dst_linesize
        );

        /*
         * 将得到的rgba数据进行回调,回调给ANativeWindow进行渲染播放
         * */
        renderCallback(dst_data[0], codecContext->width, codecContext->height, dst_linesize[0]);
        /*释放原始包,已经被渲染完了*/
        releaseAVFrame(&frame); 
    }
    /*出现错误,所退出的循环,都要释放frame*/
    releaseAVFrame(&frame);
    isPlaying = false;
    av_free(&dst_data[0]);
    sws_freeContext(sws_ctx);
}

  • 播放会涉及到 视频帧yuv转rgba耗时操作,pthread_create创建线程处理。
  • sws_getContext:初始化转换上下文SwsContext,第七个参数是yuv 转rgba flag :SWS_FAST_BILINEAR,快速双线性,速度快可能会模糊, 所以选择SWS_BILINEAR普通双线性就好。
  • sws_scale:将获取到的AVFrame数据进行数据转换,转成RGBA数据
  • renderCallback:将RGBA数据回调,通过ANativeWindow进行渲染播放

3.RGBA数据通过AnativeWindow渲染播放

3.1.初始化ANativeWindow
pthread_mutex_lock(&mutex);
/*先释放之前的显示窗口*/
if (window) {
    ANativeWindow_release(window);
    window = 0;
}

/*创建新的窗口用于视频显示*/
window = ANativeWindow_fromSurface(env, surface);

pthread_mutex_unlock(&mutex);
  • pthread_mutex_lock(&mutex):为了线程安全,加锁
  • ANativeWindow_release(window):如果之前有初始化,先释放
  • ANativeWindow_fromSurface(env, surface):从SurfaceView中获取ANativeWindow
3.2.将rgba数据渲染到ANativeWindow
/*为了线程安全加锁*/
pthread_mutex_lock(&mutex);
if (!window) {
    /*出现了问题后,释放锁,避免出现死锁问题*/
    pthread_mutex_unlock(&mutex);
}

/*设置窗口的大小,各个属性*/
ANativeWindow_setBuffersGeometry(window, width, height, WINDOW_FORMAT_RGBA_8888);

/*声明ANativeWindow缓冲区*/
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);
/*目标行大小*/
int dst_linesize = window_buffer.stride * 4;
/*
 *  图:一行一行显示 循环遍历高度
 *  视频分辨率:426 * 240
 *  视频分辨率:宽 426
 *  一行数据的大小 426 * 4(rgba8888) = 1704
 *  memcpy(dst_data + i * 1704, src_data + i * 1704, 1704); 直接这样处理会花屏幕
 *  花屏原因:ANativeWindow_Buffer 64字节对齐的算法,  1704无法以64位字节对齐
 *  FFmpeg是默认采用8字节对齐的,他就认为没有问题, 但是ANativeWindow_Buffer他是64字节对齐的,就有问题
 * */
for (int i = 0; i < window_buffer.height; ++i) {
    /*
     *C库函数 void *memcpy(void *str1, const void *str2, size_t n) 从存储区 str2 复制 n 个字节到存储区 str1。
     * 将src_data + i * src_lineSize存储区的数据 复制dst_linesize个字节 到 dst_data + i * dst_linesize存储区
     * */
    memcpy(dst_data + i * dst_linesize, src_data + i * src_lineSize, dst_linesize);
}

/*
 * 解锁后并且刷新window_buffer的数据显示画面
 * */
ANativeWindow_unlockAndPost(window);

pthread_mutex_unlock(&mutex);
  • ANativeWindow_setBuffersGeometry:设置ANativeWindow 属性,宽、高、数据格式。
  • window_buffer.stride * 4:得到目标行大小,不能直接使用src_lineSize大小,因为FFmpeg是默认采用8字节对齐的,ANativeWindow是64字节对齐的,直接使用src_lineSize会导致花屏。
  • memcpy:循环按照高度遍历将数据进行拷贝window_buffer中,然后刷新就能显示画面了。

总结:视频封装格式解码到播放流程:从队列获取AVPackage数据->解码成AVFrame数据->数据格式转化成rgba数据->将rgba数据循环遍历拷贝到ANativeWindow缓冲区->刷新就能显示出画面了。

猜你喜欢

转载自blog.csdn.net/u014078003/article/details/125370374