Android NDK开发之旅33--FFmpeg视频播放

Android NDK开发之旅 目录

1.播放多媒体文件步骤

通常情况下,我们下载的视频文件如MP4,MKV、FLV等都属于封装格式,就是把音视频数据按照相应的规范,打包成一个文本文件。我们可以使用MediaInfo这个工具查看媒体文件的相关信息。
当我们播放一个媒体文件时,通常需要经过以下几个步骤如下:

《Android NDK开发之旅33--FFmpeg视频播放》 音视频解码步骤

  • 解封装(Demuxing):就是将输入的封装格式的数据,分离成为音频流压缩编码数据和视频流压缩编码数据。封装格式种类很多,例如MP4,MKV,RMVB,TS,FLV,AVI等等,它的作用就是将已经压缩编码的视频数据和音频数据按照一定的格式放到一起。例如,FLV格式的数据,经过解封装操作后,输出H.264编码的视频码流和AAC编码的音频码流。

  • 解码(Decode):就是将视频/音频压缩编码数据,解码成为非压缩的视频/音频原始数据。音频的压缩编码标准包含AAC,MP3等,视频的压缩编码标准则包含H.264,MPEG2等。解码是整个系统中最重要也是最复杂的一个环节。通过解码,压缩编码的视频数据输出成为非压缩的颜色数据,例如YUV、RGB等等;压缩编码的音频数据输出成为非压缩的音频抽样数据,例如PCM数据。

  • 音视频同步:就是根据解封装模块处理过程中获取到的参数信息,同步解码出来的音频和视频数据,并将音视频频数据送至系统的显卡和声卡播放出来(Render)。

2.FFmpeg介绍

Android需要音/视频编解码需要用到FFmpeg的so库,请查看

Android NDK开发之旅29–云服务器Ubuntu下搭建NDK环境,并编译FFmpeg

FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。它包括了领先的音/视频编解码库libavcodec、libavformat、libswscale等。

首先介绍一下ffmpeg里面各模块的功能把:

库名工具
libavformat用于各种音视频封装格式的生成和解析,包括获取解码所需信息以生成解码上下文结构和读取音视频帧等功能;音视频的格式解析协议,为libavcodec分析码流提供独立的音频或视频码流源。
libavcodec用于各种类型声音/图像编解码;该库是音视频编解码核心,实现了市面上可见的绝大部分解码器的功能,libavcodec库被其他各大解码器ffdshow,MPlayer等所包含或应用。
libavdevice硬件采集、加速、显示。操作计算机中常用的音视频捕获或输出设备;
libavfilterfilter音视频滤波器的开发,如宽高比、剪裁、格式化、非格式化、伸缩。
libavutil包含一些公共的工具函数的使用库,包括算数运算、字符操作。
libavresample音视频封装编解码格式预设等。
libswscale(原始视频格式转换)用于视频场景比例缩放、色彩映射转换;图像颜色空间或格式转换。
libswresample原始音频格式转码
libpostproc(同步、时间计算的简单算法)用于后期效果处理;音视频应用的后期处理,如图像的去块效应。

3.FFmpeg音视频解码过程

通过上面对媒体文件播放步骤的了解,我们在解码多媒体文件的时候需要经过两个步骤,即解封装(Demuxing)和解码(Decode)。下面就来看一下FFMPEG解码媒体文件的时候是怎样做这两个步骤的。

《Android NDK开发之旅33--FFmpeg视频播放》

3.1.注册所有组件

av_register_all();

这个函数,可以注册所有支持的容器和对应的codec。

3.2.打开输入视频文件

AVFormatContext *pFormatCtx = avformat_alloc_context();
avformat_open_input(&pFormatCtx,input_cstr,NULL,NULL);

3.3.获取视频文件信息

    avformat_find_stream_info(pFormatCtx,NULL);
    //获取视频流的索引位置
    //遍历所有类型的流(音频流、视频流、字幕流),找到视频流
    int v_stream_idx = -1;
    int i = 0;
    //number of streams
    for (; i < pFormatCtx->nb_streams; i++)
    {
        //流的类型
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO)
        {
            v_stream_idx = i;
            break;
        }
    }

3.4.根据编解码上下文中的编码id查找对应的解码器

    AVCodecContext *pCodecCtx = pFormatCtx->streams[v_stream_idx]->codec;
    AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);

3.5.打开解码器

avcodec_open2(pCodecCtx,pCodec,NULL)

来打开解码器,AVFormatContext、AVStream、AVCodecContext、AVCodec四者之间的关系为

《Android NDK开发之旅33--FFmpeg视频播放》

3.6.一帧一帧读取压缩的视频数据AVPacket

while (av_read_frame(pFormatCtx, packet) >= 0) {
省略...
}

3.7.解码一帧视频压缩数据,得到视频像素数据

avcodec_decode_video2(pCodecCtx, pFrame, &got_picture, packet)

4.Android整体项目下视频播放流程

4.1输入视频路径

4.2.把视频数据解码为YUV像素数据

4.3.YUV数据转化为RGB格式。 (这一步可以省略)

4.4.一帧一帧的传给SurfaceView显示出来

注意:

其实YUV数据可直接在SurfaceView显示,在研究Android系统多媒体框架的stagefright视频显示时发现,根本找不到omx解码后的yuv是怎么转换成RGB的代码,yuv数据在render之后就找不到去向了,可画面确确实实的显示出来了。

稍微看一下AsomePlayer的代码,不难发现,视频的每一帧是通过调用了SoftwareRenderer来渲染显示的、这是一个很大的突破,以后可以直接丢yuv数据到surface显示,无需耗时耗效率的yuv转RGB了,这部分知识点会在以后的文章中实现本篇不涉及。

5.关键代码

VideoUtils.class
package com.haocai.ffmpegtest;

import android.view.Surface;

public class VideoUtils {

    public native void render(String input,Surface surface);

    static{
        System.loadLibrary("avutil-54");
        System.loadLibrary("swresample-1");
        System.loadLibrary("avcodec-56");
        System.loadLibrary("avformat-56");
        System.loadLibrary("swscale-3");
        System.loadLibrary("postproc-53");
        System.loadLibrary("avfilter-5");
        System.loadLibrary("avdevice-56");
        System.loadLibrary("myffmpeg");
    }
}

activity_simple_play.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <SurfaceView
        android:id="@+id/video_view"
        android:layout_width="fill_parent"
        android:layout_height="fill_parent"/>

</LinearLayout>
SimplePlayActivity.class
public class SimplePlayActivity extends Activity  implements SurfaceHolder.Callback {


    @BindView(R.id.video_view)
    SurfaceView videoView;
    private VideoUtils player;
    SurfaceHolder surfaceHolder;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_simple_play);
        ButterKnife.bind(this);
        player = new VideoUtils();
        surfaceHolder = videoView.getHolder();
        //surface
        surfaceHolder.addCallback(this);
    }

    @Override
    public void surfaceCreated(final SurfaceHolder holder) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                String input = new File(Environment.getExternalStorageDirectory(),"小苹果.mp4").getAbsolutePath();
                Log.d("main",input);
                player.render(input, holder.getSurface());
            }
        }).start();
    }

    @Override
    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public void surfaceDestroyed(SurfaceHolder holder) {
        holder.getSurface().release();
    }
}
ffmpeg_player.c
#include <com_haocai_ffmpegtest_VideoUtils.h>
#include <android/log.h>
#include <android/native_window_jni.h>
#include <android/native_window.h>
#include <stdio.h>
//解码
#include "include/libavcodec/avcodec.h"
//封装格式处理
#include "include/libavformat/avformat.h"
//像素处理
#include "include/libswscale/swscale.h"

#define  LOG_TAG    "ffmpegandroidplayer"
#define  LOGI(FORMAT,...) __android_log_print(ANDROID_LOG_INFO,LOG_TAG,FORMAT,##__VA_ARGS__);
#define  LOGE(FORMAT,...) __android_log_print(ANDROID_LOG_ERROR,LOG_TAG,FORMAT,##__VA_ARGS__);
#define  LOGD(FORMAT,...)  __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG,FORMAT, ##__VA_ARGS__)

JNIEXPORT void JNICALL Java_com_haocai_ffmpegtest_VideoUtils_render
  (JNIEnv *env, jobject jobj, jstring input_jstr, jobject surface){
    const char* file_name = (*env)->GetStringUTFChars(env, input_jstr, NULL);


    LOGD("play");


    av_register_all();

    AVFormatContext *pFormatCtx = avformat_alloc_context();

    // Open video file
    if (avformat_open_input(&pFormatCtx, file_name, NULL, NULL) != 0) {

        LOGD("Couldn't open file:%s\n", file_name);
        return ; // Couldn't open file
    }

    // Retrieve stream information
    if (avformat_find_stream_info(pFormatCtx, NULL) < 0) {
        LOGD("Couldn't find stream information.");
        return ;
    }

    // Find the first video stream
    int videoStream = -1, i;
    for (i = 0; i < pFormatCtx->nb_streams; i++) {
        if (pFormatCtx->streams[i]->codec->codec_type == AVMEDIA_TYPE_VIDEO
            && videoStream < 0) {
            videoStream = i;
        }
    }
    if (videoStream == -1) {
        LOGD("Didn't find a video stream.");
        return ; // Didn't find a video stream
    }

    // Get a pointer to the codec context for the video stream
    AVCodecContext *pCodecCtx = pFormatCtx->streams[videoStream]->codec;

    // Find the decoder for the video stream
    AVCodec *pCodec = avcodec_find_decoder(pCodecCtx->codec_id);
    if (pCodec == NULL) {
        LOGD("Codec not found.");
        return ; // Codec not found
    }

    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        LOGD("Could not open codec.");
        return ; // Could not open codec
    }

    // 获取native window
    ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);

    // 获取视频宽高
    int videoWidth = pCodecCtx->width;
    int videoHeight = pCodecCtx->height;

    // 设置native window的buffer大小,可自动拉伸
    ANativeWindow_setBuffersGeometry(nativeWindow, videoWidth, videoHeight,
                                     WINDOW_FORMAT_RGBA_8888);
    ANativeWindow_Buffer windowBuffer;

    if (avcodec_open2(pCodecCtx, pCodec, NULL) < 0) {
        LOGD("Could not open codec.");
        return ; // Could not open codec
    }

    // Allocate video frame
    AVFrame *pFrame = av_frame_alloc();

    // 用于渲染
    AVFrame *pFrameRGBA = av_frame_alloc();
    if (pFrameRGBA == NULL || pFrame == NULL) {
        LOGD("Could not allocate video frame.");
        return ;
    }

    // Determine required buffer size and allocate buffer
    // buffer中数据就是用于渲染的,且格式为RGBA
    int numBytes = av_image_get_buffer_size(AV_PIX_FMT_RGBA, pCodecCtx->width, pCodecCtx->height,
                                            1);
    uint8_t *buffer = (uint8_t *) av_malloc(numBytes * sizeof(uint8_t));
    av_image_fill_arrays(pFrameRGBA->data, pFrameRGBA->linesize, buffer, AV_PIX_FMT_RGBA,
                         pCodecCtx->width, pCodecCtx->height, 1);

    // 由于解码出来的帧格式不是RGBA的,在渲染之前需要进行格式转换
    struct SwsContext *sws_ctx = sws_getContext(pCodecCtx->width,
                                                pCodecCtx->height,
                                                pCodecCtx->pix_fmt,
                                                pCodecCtx->width,
                                                pCodecCtx->height,
                                                AV_PIX_FMT_RGBA,
                                                SWS_BILINEAR,
                                                NULL,
                                                NULL,
                                                NULL);

    int frameFinished;
    AVPacket packet;
    while (av_read_frame(pFormatCtx, &packet) >= 0) {
        // Is this a packet from the video stream?
        if (packet.stream_index == videoStream) {

            // Decode video frame
            avcodec_decode_video2(pCodecCtx, pFrame, &frameFinished, &packet);

            // 并不是decode一次就可解码出一帧
            if (frameFinished) {

                // lock native window buffer
                ANativeWindow_lock(nativeWindow, &windowBuffer, 0);

                // 格式转换
                sws_scale(sws_ctx, (uint8_t const *const *) pFrame->data,
                          pFrame->linesize, 0, pCodecCtx->height,
                          pFrameRGBA->data, pFrameRGBA->linesize);

                // 获取stride
                uint8_t *dst = (uint8_t *) windowBuffer.bits;
                int dstStride = windowBuffer.stride * 4;
                uint8_t *src = (pFrameRGBA->data[0]);
                int srcStride = pFrameRGBA->linesize[0];

                // 由于window的stride和帧的stride不同,因此需要逐行复制
                int h;
                for (h = 0; h < videoHeight; h++) {
                    memcpy(dst + h * dstStride, src + h * srcStride, srcStride);
                }

                ANativeWindow_unlockAndPost(nativeWindow);
                //延时绘制 否则视频快速播放
                usleep(1000 * 16);
            }

        }
        av_packet_unref(&packet);
    }

    av_free(buffer);
    av_free(pFrameRGBA);

    // Free the YUV frame
    av_free(pFrame);

    // Close the codecs
    avcodec_close(pCodecCtx);

    // Close the video file
    avformat_close_input(&pFormatCtx);
    (*env)->ReleaseStringUTFChars(env, input_jstr, file_name);
    return ;
}
说明:其它视频格式也支持

6.播放效果

《Android NDK开发之旅33--FFmpeg视频播放》

源码下载

Github:https://github.com/kpioneer123/FFmpegTest

特别感谢:

CrazyDiode
小码哥_WS

《Android NDK开发之旅33--FFmpeg视频播放》 微信号kpioneer

    原文作者:香沙小熊
    原文地址: https://www.jianshu.com/p/6b943ff8fec8
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞