31.FFmpeg+OpenGLES+OpenSLES播放器实现(五.FFmpeg解封装)

简介: 项目源码FFmpeg开发文档 Android Studio的开发环境已经准备好,接下来开始正式的写ndk代码,首先创建一个FFmpeg工具类,添加native方法 import android.

项目源码
FFmpeg开发文档
Android Studio的开发环境已经准备好,接下来开始正式的写ndk代码,首先创建一个FFmpeg工具类,添加native方法

import android.view.Surface;
public class FFmpegPlayer {
    static {
        System.loadLibrary("ffmpeg");
    }
    /**
     * 播放视频
     */
    public native void playVideo(String url,Surface surface);

}

传入的Surface对象是用于显示播放界面的,这里先传入,后边再说

然后生成对应的头文件,之前在eclipse上开发的时候都是通过javah命令生成头文件后将对应的方法名拷贝到我们的c文件中,这部分的生成方法可以查看之前的博客。现在我们使用的Android Studio3.x可以一键生成对应的native方法,将光标选中方法名然后alt+enter选择create function xxx即可

extern "C"
JNIEXPORT void JNICALL
Java_com_rzm_ffmpegplayer_FFmpegPlayer_playVideo(JNIEnv *env, jobject instance, jstring url_,
                                                 jobject surface) {
    const char *url = env->GetStringUTFChars(url_, 0);

    env->ReleaseStringUTFChars(url_, url);
}

今天来进行的是FFmpeg的第一部分,解封装

解封装

一个mp4文件可能是视频流音频流字幕流等等多个流的一个结合体,而我们要实现视频和音频的播放,就需要将这个结合体拆分开来,单独的进行视频播放,音频播放,实现同步,这是实现播放的一个必要的条件。所以播放的第一步就要进行源文件的解封装,解封装涉及到的一些接口如下

av_register_all()

初始化libavformat 并注册所有的muxers和demuxers以及各种协议,当然,如果你只需要初始化特定的支持组件,那么单独调用特定的方法即可,一般直接这样做一劳永逸

avformat_open_input

打开输入流(可以是本地流也可以是网络流,如果是网络的,那么需要avformat_network_init()方法支持)并读取视频头信息,视频头通常包含一些视频基本信息,比如说视频格式,streams的数量等等。注意此时编解码器未打开,最后必须使用avformat_close_input(&avFormatContext)进行关闭,避免造成泄漏

avformat_network_init()

初始化全局的网络组件,支持rtfp协议的时候需要打开这个开关,ffmpeg推荐打开这个全局的网络开关,因为可以减少单独对每个单独回话做设置的开销

avformat_find_stream_info()

打开流文件之后执行这个方法,读取媒体文件获取到流信息,这个方法对于没有视频头的视频尤其有效(MPEG-2),,此方法执行之后会将读取到的流信息填充到AVFormatContext中的各个流的轨道上,这样一来,AVFormatContext->streams[i]中就有信息了.这个方法可以打开部分解码器并保存在第二个参数AVDictionary中,但是他无法保证打开全部的解码器,如果存在null那么也是正常现象,这里第二个参数我们直接置为NULL

av_find_best_stream

媒体信息已经获取到了,接下来需要将视频流和音频流区分开来,可以通过av_find_best_stream来获取流轨道的index,在老版本的ffmpeg上我们是通过遍历所有的流通过对比codec_type来判断的,代码如下

    for(int i = 0; i < avFormatContext->nb_streams; i++){
        AVStream *avStream = avFormatContext->streams[i];
        if (avStream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
            //找到视频index
            videoIndex = i;
        } else if (avStream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
            //找到音频index
            audioIndex = i;            
        }
    }

通过这个方法获取的方式为下,验证一下,你会发现二者结果相同

videoIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);
audioIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);
av_read_frame

返回流的下一帧,这个函数返回文件中存储的内容。每调用一次,它就将返回一帧数据,这是从文件存储的内容中切割出来的

av_packet_unref

这个方法会擦除packet空间,不再指向这个packet的缓存空间,另外也会将packet重置为默认状态。一帧数据也就是一个ACPacket使用完之后需要回收,否则会造成内存急剧增长,下边分别是调用这个方法和不调用这个方法的内存变化状态


img_569d2f2ef61d75b696c8d03bf1806436.png
调用.png

img_512281ce7cbb1382f29070af4516220b.png
不调用.png
完整的代码如下
/**
 * 播放视频,支持本地和网络两种
 */
extern "C"
JNIEXPORT void JNICALL
Java_com_rzm_ffmpegplayer_FFmpegPlayer_playVideo(JNIEnv *env, jobject instance, jstring url_,
                                                 jobject surface) {
    const char *url = env->GetStringUTFChars(url_, 0);

    //初始化解封装
    av_register_all();
    //初始化全局网络组件,可选推荐使用,在使用网络协议的场景中这是必选的(rtfp http)
    avformat_network_init();

    AVFormatContext *avFormatContext = NULL;
    //指定输入的格式,如果为NULL,将自动检测输入格式,所以可置为NULL
    //AVInputFormat *fmt = NULL;
    //打开输入文件,可以是本地视频或者网络视频
    int result = avformat_open_input(&avFormatContext,url,NULL,NULL);

    //打开输入内容失败
    if(result != 0){
        LOGE("avformat_open_input failed!:%s",av_err2str(result));
        return;
    }

    //打开输入成功
    LOGI("avformat_open_input success!:%s",av_err2str(result));

    //读取媒体文件的分组以获得流信息。这个对于没有标题的文件格式(如MPEG)很有用。这个函数还计算实际的帧率在
    //MPEG-2重复的情况下帧模式。
    result = avformat_find_stream_info(avFormatContext,NULL);
    if (result < 0){
        LOGE("avformat_find_stream_info failed: %s",av_err2str(result));
    }

    //获取到了输入文件信息,打印一下视频时长和nb_streams
    LOGI("duration = %lld nb_streams=%d",avFormatContext->duration,avFormatContext->nb_streams);

    //分离音视频,获取音视频在源文件中的streams index
    int videoIndex = 0;
    int audioIndex = 1;
    int fps = 0;
    for(int i = 0; i < avFormatContext->nb_streams; i++){
        AVStream *avStream = avFormatContext->streams[i];
        if (avStream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
            //找到视频index
            videoIndex = i;
            LOGI("video index = %d",videoIndex);
            //FPS是图像领域中的定义,是指画面每秒传输帧数
            fps = r2d(avStream->avg_frame_rate);

            LOGI("video info ---- fps = %d fps den= %d fps num= %d width=%d height=%d code id=%d format=%d",
                 fps,
                 avStream->avg_frame_rate.den,
                 avStream->avg_frame_rate.num,
                 avStream->codecpar->width,
                 avStream->codecpar->height,
                 avStream->codecpar->codec_id,
                 avStream->codecpar->format
            );

        } else if (avStream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
            //找到音频index
            audioIndex = i;
            LOGI("audio index = %d sampe_rate=%d channels=%d sample_format=%d",
                 audioIndex,
                 avStream->codecpar->sample_rate,
                 avStream->codecpar->channels,
                 avStream->codecpar->format
            );
        }
    }

    //上边通过遍历streams音视频的index,还可以通过提供的接口获取
    videoIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_VIDEO,-1,-1,NULL,0);
    audioIndex = av_find_best_stream(avFormatContext,AVMEDIA_TYPE_AUDIO,-1,-1,NULL,0);
    LOGI("av_find_best_stream videoIndex=%d audioIndex=%d",videoIndex,audioIndex);

    //读取帧数据

    //Allocate an AVPacket and set its fields to default values
    //存储压缩数据,对于视频,它通常应该包含一个压缩帧。对于音频它可能包含几个压缩帧
    AVPacket *avPacket = av_packet_alloc();
    for (;;) {

        //Return the next frame of a stream.
        int read_result = av_read_frame(avFormatContext,avPacket);
        if(read_result != 0){
            //读取到结尾处,从20秒位置继续开始播放
            LOGI("读取到结尾处 %s",av_err2str(read_result));
            //跳转到指定的position播放,最后一个参数表示
            //int pos = 200000 * r2d(avFormatContext->streams[videoIndex]->time_base);
            //av_seek_frame(avFormatContext,videoIndex,pos,AVSEEK_FLAG_BACKWARD|AVSEEK_FLAG_FRAME );
            //LOGI("avFormatContext->streams[videoIndex]->time_base= %d",avFormatContext->streams[videoIndex]->time_base);
            //continue;
            break;
        }
        LOGW("stream = %d size =%d pts=%lld flag=%d pos = %d",
             avPacket->stream_index,avPacket->size,avPacket->pts,avPacket->flags,avPacket->pos
        );

        //packet使用完成之后执行,否则内存会急剧增长
        //不再引用这个packet指向的空间,并且将packet置为default状态
        av_packet_unref(avPacket);
    }

    //关闭上下文
    avformat_close_input(&avFormatContext);
    env->ReleaseStringUTFChars(url_, url);
}
相关文章
|
7月前
|
存储 编解码 算法
探索FFmpeg复用:深入理解媒体数据的组织与封装(三)
探索FFmpeg复用:深入理解媒体数据的组织与封装
141 0
|
存储 编解码 Cloud Native
音视频 ffmpeg命令转封装
音视频 ffmpeg命令转封装
|
存储 编解码 Linux
FFmpeg+SDL播放器开发实践:解析、解码、渲染全流程详解
FFmpeg+SDL播放器开发实践:解析、解码、渲染全流程详解
|
2月前
|
Linux 开发工具 Android开发
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
ijkplayer是由Bilibili基于FFmpeg3.4研发并开源的播放器,适用于Android和iOS,支持本地视频及网络流媒体播放。本文详细介绍如何在新版Android Studio中导入并使用ijkplayer库,包括Gradle版本及配置更新、导入编译好的so文件以及添加直播链接播放代码等步骤,帮助开发者顺利进行App调试与开发。更多FFmpeg开发知识可参考《FFmpeg开发实战:从零基础到短视频上线》。
229 2
FFmpeg开发笔记(六十)使用国产的ijkplayer播放器观看网络视频
|
7月前
|
编解码 API 数据处理
【摄像头数据处理】摄像头数据处理:使用FFmpeg合并、编码和封装视频流
【摄像头数据处理】摄像头数据处理:使用FFmpeg合并、编码和封装视频流
410 0
|
7月前
|
存储 编解码 安全
探索FFmpeg复用:深入理解媒体数据的组织与封装(二)
探索FFmpeg复用:深入理解媒体数据的组织与封装
154 0
|
7月前
|
存储 编解码 算法
探索FFmpeg复用:深入理解媒体数据的组织与封装(一)
探索FFmpeg复用:深入理解媒体数据的组织与封装
214 0
|
7月前
|
设计模式 存储 缓存
【ffmpeg C++ 播放器优化实战】优化你的视频播放器:使用策略模式和单例模式进行视频优化
【ffmpeg C++ 播放器优化实战】优化你的视频播放器:使用策略模式和单例模式进行视频优化
227 0
|
7月前
|
存储 算法 前端开发
深入理解FFmpeg音视频编程:处理封装、解码、播放 队列与回放策略
深入理解FFmpeg音视频编程:处理封装、解码、播放 队列与回放策略
326 0
|
7月前
|
人机交互 C++
QT + FFmpeg 5.x + x264 + x265 + SDL2 音视频播放器
QT + FFmpeg 5.x + x264 + x265 + SDL2 音视频播放器
237 0