Android 使用 FFmpeg (二)——视屏流播放简单实现

来源:互联网 发布:linux下载安装jdk1.8 编辑:程序博客网 时间:2024/06/05 12:15

    如标题所示这一篇主要实现简单的实现视屏流,视屏流,视屏流的播放实现,而不是“视屏播放“,所以不会涉及到“音频流“,或者“字幕流“的播放。放张图简单说明一下:

视频截图
如上图标注的地方“Stream 0“——视屏流,“Stream 1“——音频流。实际视屏文件中可能还会有“字幕流“,但这又涉及到“内置字幕/硬字幕“和“外挂字幕“的相关知识,这些不在本文讨论范围内(上图特意找了一个有字幕但没有“字幕流“的视屏截图,也是为了说明这种情况——“硬字幕“直接内嵌在视屏流当中,没有单独的“字幕流“)。
    如果对这样的说法有些不理解,个人推荐一篇博客《[总结]视音频编解码技术零基础学习方法》
另外其实雷神其他相关音视频方面的博客也都强烈推荐,相信看完他的文章你会收益更多。
    本篇文章是基于我的上一篇Android 使用 FFmpeg (一)——编译生成.so文件来写的但在项目目录结构上有所改动,所以有需要的话可以去看一下。


一,项目结构

项目结构目录改动

如上图

FFmpegNativeUtil类只是简单加载动态库,和声明一个native方法如下

public class FFmpegNativeUtil {    static {        System.loadLibrary("avcodec-57");        System.loadLibrary("avdevice-57");        System.loadLibrary("avfilter-6");        System.loadLibrary("avformat-57");        System.loadLibrary("avutil-55");        System.loadLibrary("postproc-54");        System.loadLibrary("swresample-2");        System.loadLibrary("swscale-4");        System.loadLibrary("native-lib");    }    /**     * 播放视频流     * @param videoPath(本地)视频文件路径     * @param surface     */    public native void videoStreamPlay(String videoPath, Surface surface);}

MyVideoView类继承SurfaceView 用来绘制视频图像。

public class MyVideoView extends SurfaceView {    FFmpegNativeUtil util;    Surface surface;    public MyVideoView(Context context) {        this(context,null);    }    public MyVideoView(Context context, AttributeSet attrs) {        this(context, attrs,0);    }    public MyVideoView(Context context, AttributeSet attrs, int defStyleAttr) {        super(context, attrs, defStyleAttr);        init();    }    private void init(){        getHolder().setFormat(PixelFormat.RGBA_8888);        surface= getHolder().getSurface();        util=new FFmpegNativeUtil();    }    /**     * 开始播放     * @param videoPath     */    public void startPlay(final String videoPath){        new Thread(new Runnable() {            @Override            public void run() {                Log.d("MyVideoView","------>>调用native方法");                util.videoStreamPlay(videoPath,surface);            }        }).start();    }}

    activity_video_stream_layout.xml布局文件只是简单使用了MyVideoView控件进行的布局。但因为这里没有对视频图像显示的实际尺寸进行缩放处理我特意将控件的高度设置为200dp,也就是说控件有多大最终视频图像就有多大——所以会有拉伸/压缩的情况。

<com.yj.ffmpegdemo.common.MyVideoView        android:id="@+id/myVV"        android:layout_width="match_parent"        android:layout_height="200dp" />

    最关键的部分是native-lib.cpp的代码,具体描述已经写在备注中:

#include <jni.h>#include <string>#include <android/log.h>#include <android/native_window_jni.h>#include <unistd.h>extern "C"{#include "libavformat/avformat.h"#include "libswscale/swscale.h"#include <libavutil/imgutils.h>}#define LOGD(FORMAT, ...) __android_log_print(ANDROID_LOG_DEBUG,"YJ_FFMPGE_DEMO------>>",FORMAT,##__VA_ARGS__);extern "C"JNIEXPORT void JNICALLJava_com_yj_ffmpegdemo_common_FFmpegNativeUtil_videoStreamPlay(JNIEnv *env, jobject instance,                                                               jstring videoPath, jobject surface) {    const char *input = env->GetStringUTFChars(videoPath, NULL);    if (input == NULL) {        LOGD("字符串转换失败......");        return;    }    //注册FFmpeg所有编解码器,以及相关协议。    av_register_all();    //分配结构体    AVFormatContext *formatContext = avformat_alloc_context();    //打开视频数据源。由于Android 对SDK存储权限的原因,如果没有为当前项目赋予SDK存储权限,打开本地视频文件时会失败    int open_state = avformat_open_input(&formatContext, input, NULL, NULL);    if (open_state < 0) {        char errbuf[128];        if (av_strerror(open_state, errbuf, sizeof(errbuf)) == 0){            LOGD("打开视频输入流信息失败,失败原因: %s", errbuf);        }        return;    }    //为分配的AVFormatContext 结构体中填充数据    if (avformat_find_stream_info(formatContext, NULL) < 0) {        LOGD("读取输入的视频流信息失败。");        return;    }    int video_stream_index = -1;//记录视频流所在数组下标    LOGD("当前视频数据,包含的数据流数量:%d", formatContext->nb_streams);    //找到"视频流".AVFormatContext 结构体中的nb_streams字段存储的就是当前视频文件中所包含的总数据流数量——    //视频流,音频流,字幕流    for (int i = 0; i < formatContext->nb_streams; i++) {        //如果是数据流的编码格式为AVMEDIA_TYPE_VIDEO——视频流。        if (formatContext->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {            video_stream_index = i;//记录视频流下标            break;        }    }    if (video_stream_index == -1) {        LOGD("没有找到 视频流。");        return;    }    //通过编解码器的id——codec_id 获取对应(视频)流解码器    AVCodecParameters *codecParameters=formatContext->streams[video_stream_index]->codecpar;    AVCodec *videoDecoder = avcodec_find_decoder(codecParameters->codec_id);    if (videoDecoder == NULL) {        LOGD("未找到对应的流解码器。");        return;    }    //通过解码器分配(并用  默认值   初始化)一个解码器context    AVCodecContext *codecContext = avcodec_alloc_context3(videoDecoder);    if (codecContext == NULL) {        LOGD("分配 解码器上下文失败。");        return;    }    //更具指定的编码器值填充编码器上下文    if(avcodec_parameters_to_context(codecContext,codecParameters)<0){        LOGD("填充编解码器上下文失败。");        return;    }    //通过所给的编解码器初始化编解码器上下文    if (avcodec_open2(codecContext, videoDecoder, NULL) < 0) {        LOGD("初始化 解码器上下文失败。");        return;    }    AVPixelFormat dstFormat = AV_PIX_FMT_RGBA;    //分配存储压缩数据的结构体对象AVPacket    //如果是视频流,AVPacket会包含一帧的压缩数据。    //但如果是音频则可能会包含多帧的压缩数据    AVPacket *packet = av_packet_alloc();    //分配解码后的每一数据信息的结构体(指针)    AVFrame *frame = av_frame_alloc();    //分配最终显示出来的目标帧信息的结构体(指针)    AVFrame *outFrame = av_frame_alloc();    uint8_t *out_buffer = (uint8_t *) av_malloc(            (size_t) av_image_get_buffer_size(dstFormat, codecContext->width, codecContext->height,                                              1));    //更具指定的数据初始化/填充缓冲区    av_image_fill_arrays(outFrame->data, outFrame->linesize, out_buffer, dstFormat,                         codecContext->width, codecContext->height, 1);    //初始化SwsContext    SwsContext *swsContext = sws_getContext(            codecContext->width   //原图片的宽            ,codecContext->height  //源图高            ,codecContext->pix_fmt //源图片format            ,codecContext->width  //目标图的宽            ,codecContext->height  //目标图的高            ,dstFormat,SWS_BICUBIC            , NULL, NULL, NULL    );    if(swsContext==NULL){        LOGD("swsContext==NULL");        return;    }    //Android 原生绘制工具    ANativeWindow *nativeWindow = ANativeWindow_fromSurface(env, surface);    //定义绘图缓冲区    ANativeWindow_Buffer outBuffer;    //通过设置宽高限制缓冲区中的像素数量,而非屏幕的物流显示尺寸。    //如果缓冲区与物理屏幕的显示尺寸不相符,则实际显示可能会是拉伸,或者被压缩的图像    ANativeWindow_setBuffersGeometry(nativeWindow, codecContext->width, codecContext->height,                                     WINDOW_FORMAT_RGBA_8888);    //循环读取数据流的下一帧    while (av_read_frame(formatContext, packet) == 0) {        if (packet->stream_index == video_stream_index) {            //讲原始数据发送到解码器            int sendPacketState = avcodec_send_packet(codecContext, packet);            if (sendPacketState == 0) {                int receiveFrameState = avcodec_receive_frame(codecContext, frame);                if (receiveFrameState == 0) {                    //锁定窗口绘图界面                    ANativeWindow_lock(nativeWindow, &outBuffer, NULL);                    //对输出图像进行色彩,分辨率缩放,滤波处理                    sws_scale(swsContext, (const uint8_t *const *) frame->data, frame->linesize, 0,                              frame->height, outFrame->data, outFrame->linesize);                    uint8_t *dst = (uint8_t *) outBuffer.bits;                    //解码后的像素数据首地址                    //这里由于使用的是RGBA格式,所以解码图像数据只保存在data[0]中。但如果是YUV就会有data[0]                    //data[1],data[2]                    uint8_t *src = outFrame->data[0];                    //获取一行字节数                    int oneLineByte = outBuffer.stride * 4;                    //复制一行内存的实际数量                    int srcStride = outFrame->linesize[0];                    for (int i = 0; i < codecContext->height; i++) {                        memcpy(dst + i * oneLineByte, src + i * srcStride, srcStride);                    }                    //解锁                    ANativeWindow_unlockAndPost(nativeWindow);                    //进行短暂休眠。如果休眠时间太长会导致播放的每帧画面有延迟感,如果短会有加速播放的感觉。                    //一般一每秒60帧——16毫秒一帧的时间进行休眠                    usleep(1000 * 20);//20毫秒                } else if (receiveFrameState == AVERROR(EAGAIN)) {                    LOGD("从解码器-接收-数据失败:AVERROR(EAGAIN)");                } else if (receiveFrameState == AVERROR_EOF) {                    LOGD("从解码器-接收-数据失败:AVERROR_EOF");                } else if (receiveFrameState == AVERROR(EINVAL)) {                    LOGD("从解码器-接收-数据失败:AVERROR(EINVAL)");                } else {                    LOGD("从解码器-接收-数据失败:未知");                }            } else if (sendPacketState == AVERROR(EAGAIN)) {//发送数据被拒绝,必须尝试先读取数据                LOGD("向解码器-发送-数据包失败:AVERROR(EAGAIN)");//解码器已经刷新数据但是没有新的数据包能发送给解码器            } else if (sendPacketState == AVERROR_EOF) {                LOGD("向解码器-发送-数据失败:AVERROR_EOF");            } else if (sendPacketState == AVERROR(EINVAL)) {//遍解码器没有打开,或者当前是编码器,也或者需要刷新数据                LOGD("向解码器-发送-数据失败:AVERROR(EINVAL)");            } else if (sendPacketState == AVERROR(ENOMEM)) {//数据包无法压如解码器队列,也可能是解码器解码错误                LOGD("向解码器-发送-数据失败:AVERROR(ENOMEM)");            } else {                LOGD("向解码器-发送-数据失败:未知");            }        }        av_packet_unref(packet);    }    //内存释放    ANativeWindow_release(nativeWindow);    av_frame_free(&outFrame);    av_frame_free(&frame);    av_packet_free(&packet);    avcodec_free_context(&codecContext);    avformat_close_input(&formatContext);    avformat_free_context(formatContext);    env->ReleaseStringUTFChars(videoPath, input);}

二,运行结果

这里写图片描述

原创粉丝点击