Android使用FFmpeg播放视频(一):视频播放

目录

相关文章

Android集成FFmpeg

效果展示

实现流程

实现步骤

1.布局添加SurfaceView用于显示视频


    
    
2.将Surface传给NDK层
public class MainActivity extends AppCompatActivity{
    private SurfaceView surfaceView;
    private Button button;
    static {
        System.loadLibrary("native-lib");
    }
    private SurfaceHolder mHolder;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        surfaceView = findViewById(R.id.sfv_player);
        button = findViewById(R.id.bt_play);
        button.setOnClickListener(v->{
            //找到SD卡中的视频文件
            File video = new File(getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS),"001.mp4");
            //子线程进行视频渲染
            new Thread(new Runnable() {
                @Override
                public void run() {
                    native_start_play(video.getAbsolutePath(),mHolder.getSurface());
                }
            }).start();
        });
        initSurfaceHolder();
    }

    private void initSurfaceHolder() {
        mHolder = surfaceView.getHolder();
        mHolder.setFormat(PixelFormat.RGBA_8888);
    }

    /**
     * 调用NDK的视频渲染
     * @param path 播放的视频的路径
     * @param surface 要渲染的surface
     * @return
     */
    public native void native_start_play(String path, Surface surface);
}
3.NDK层进行视频流渲染

这里的逻辑是根据实现流程来的,每一步都加入了注释

#include 
#include 
#include 
#include 
//混合C代码编译
extern "C"{
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libswscale/swscale.h"
#include "libavutil/imgutils.h"
}
extern "C"
JNIEXPORT void JNICALL
Java_com_itfitness_ffmpegdemo_MainActivity_native_1start_1play(JNIEnv *env, jobject thiz,
                                                               jstring path, jobject surface) {
    //获取用于绘制的NativeWindow
    ANativeWindow *a_native_window = ANativeWindow_fromSurface(env,surface);

    //转换视频路径字符串为C中可用的
    const char *video_path = env->GetStringUTFChars(path,0);

    //网络模块初始化(可以播放Url)
    avformat_network_init();

    //获取用于获取视频文件中各种流(视频流、音频流、字幕流等)的上下文:AVFormatContext
    AVFormatContext *av_format_context = avformat_alloc_context();

    //配置信息
    AVDictionary *options = NULL;
    av_dict_set(&options,"timeout","3000000",0);

    //打开视频文件
    //第一个参数:AVFormatContext的二级指针
    //第二个参数:视频路径
    //第三个参数:非NULL的话就是设置输入格式,NULL就是自动
    //第四个参数:配置项
    //返回值是是否打开成功,0是成功其他为失败
    int open_result = avformat_open_input(&av_format_context, video_path, NULL, &options);

    //如果打开失败就返回
    if(open_result){
        return;
    }

    //让FFmpeg将流解析出来,并找到视频流对应的索引
    avformat_find_stream_info(av_format_context, NULL);
    int video_stream_index = 0;
    for(int i = 0; i < av_format_context->nb_streams ; i++){
        //如果当前流是视频流的话保存索引
        if(av_format_context->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
            video_stream_index = i;
            break;
        }
    }

    //获取视频流的解码参数(宽高等信息)
    AVCodecParameters * av_codec_parameters = av_format_context->streams[video_stream_index]->codecpar;

    //获取视频流的解码器
    AVCodec *av_codec = avcodec_find_decoder(av_codec_parameters->codec_id);

    //获取解码上下文
    AVCodecContext * av_codec_context = avcodec_alloc_context3(av_codec);

    //将解码器参数复制到解码上下文(因为解码上下文目前还没有解码器参数)
    avcodec_parameters_to_context(av_codec_context,av_codec_parameters);

    //进行解码
    avcodec_open2(av_codec_context,av_codec,NULL);

    //因为YUV数据被封装在了AVPacket中,因此我们需要用AVPacket去获取数据
    AVPacket *av_packet = av_packet_alloc();

    //获取转换上下文(把解码后的YUV数据转换为RGB数据才能在屏幕上显示)
    SwsContext *sws_context = sws_getContext(av_codec_context->width,av_codec_context->height,av_codec_context->pix_fmt,
                   av_codec_context->width,av_codec_context->height,AV_PIX_FMT_RGBA,SWS_BILINEAR,
                   0,0,0);

    //设置NativeWindow绘制的缓冲区
    ANativeWindow_setBuffersGeometry(a_native_window,av_codec_context->width,av_codec_context->height,
                                     WINDOW_FORMAT_RGBA_8888);
    //绘制时,用于接收的缓冲区
    ANativeWindow_Buffer a_native_window_buffer;

    //计算出转换为RGB所需要的容器的大小
    //接收的容器
    uint8_t *dst_data[4];
    //每一行的首地址(R、G、B、A四行)
    int dst_line_size[4];
    //进行计算
    av_image_alloc(dst_data,dst_line_size,av_codec_context->width,av_codec_context->height,
                   AV_PIX_FMT_RGBA,1);

    //从视频流中读数据包,返回值小于0的时候表示读取完毕
    while (av_read_frame(av_format_context,av_packet) >= 0){
        //将取出的数据发送出来
        avcodec_send_packet(av_codec_context,av_packet);

        //接收发送出来的数据
        AVFrame *av_frame = av_frame_alloc();
        int av_receive_result = avcodec_receive_frame(av_codec_context,av_frame);

        //如果读取失败就重新读
        if(av_receive_result == AVERROR(EAGAIN)){
            continue;
        } else if(av_receive_result < 0){
            //如果到末尾了就结束循环读取
            break;
        }

        //将取出的数据放到之前定义的RGB目标容器中
        sws_scale(sws_context,av_frame->data,av_frame->linesize,0,av_frame->height,
                  dst_data,dst_line_size);

        //加锁然后进行渲染
        ANativeWindow_lock(a_native_window,&a_native_window_buffer,0);

        uint8_t *first_window = static_cast(a_native_window_buffer.bits);
        uint8_t *src_data = dst_data[0];

        //拿到每行有多少个RGBA字节
        int dst_stride = a_native_window_buffer.stride * 4;
        int src_line_size = dst_line_size[0];
        //循环遍历所得到的缓冲区数据
        for(int i = 0; i < a_native_window_buffer.height;i++){
            //内存拷贝进行渲染
            memcpy(first_window+i*dst_stride,src_data+i*src_line_size,dst_stride);
        }

        //绘制完解锁
        ANativeWindow_unlockAndPost(a_native_window);

        //40000微秒之后解析下一帧(这个是根据视频的帧率来设置的,我这播放的视频帧率是25帧/秒)
        usleep(1000 * 40);
        //释放资源
        av_frame_free(&av_frame);
        av_free_packet(av_packet);
    }

    env->ReleaseStringUTFChars(path,video_path);
}

注意

●视频没声音

这里只是渲染了视频的画面数据,并没有进行声音的处理,因此没有声音是正常的

●每一帧画面的延迟时间

每一帧画面渲染的延迟时间是根据视频的信息来设置的,我案例中的视频是25帧/秒,换算出来是一帧40000微秒,否则会出现视频画面播放过快或过慢的问题


优化补充

●视频帧的延迟优化

之前的延迟时间是固定写死的这样来说不大好,因为每个视频的帧率可能不一样,这样就不能适配所有的视频,因此这里我们优化下,使用FFmpeg的API获取到视频的帧率然后计算出每帧的延迟时间,具体如下,我们通过编解码上下文获取到视频流里的avg_frame_rate其中frame_rate.num其实就是视频的帧率(因为frame_rate.den一般是1),不过我们还是用两个数值来计算下,然后我们再通过这个帧率来计算出每帧的延迟时间即可

//计算出视频的帧率
    AVRational frame_rate =  avFormatContext->streams[video_stream_id]->avg_frame_rate;
    double fps = frame_rate.num / frame_rate.den;
    //计算出延时时间(单位:微秒)
    double delayTime = 1.0f / fps * 1000000;

最后延时哪里也需要改下


案例源码

https://gitee.com/itfitness/ffmpeg-build

你可能感兴趣的:(Android使用FFmpeg播放视频(一):视频播放)