实现android ffmpeg播放视频

前面两篇文章已经可以生成so,并且可以在Android studio中通过配置可以运行,并且可以打印出ffmpeg的配置信息。

准备:

在进行播放视频时需要一个吧yuv图像数据转换成argb的图像数据,这里还需要一个libyuv.so 的一个so库。可以通过这篇文章生成这个so :https://blog.csdn.net/quan648997767/article/details/70880284

一切准备就绪。

下面就是要在Android手机上通过ffmpeg播放视频。

1.首先准备一个用于显示视频画面的SurfaceView,并通过这个SurfaceView调用native方法,实现播放。

class MyVideoView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) :
    SurfaceView(context, attrs, defStyleAttr) {
    lateinit var surface: Surface

    init {
        init()
    }

    private fun init() {
        holder.setFormat(PixelFormat.RGBA_8888)
        surface = holder.surface
    }

    /**
     * 开始播放
     * @param videoPath
     */
    fun startPlay(videoPath: String) {
        Thread(Runnable {
            Log.d("MyVideoView", "------>>调用native方法")
            play(videoPath, surface)
        }).start()
    }
    private external fun play (path:String, surface: Surface)
    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }


}

2.C++解码、转换、播放代码

extern "C"
JNIEXPORT void JNICALL
Java_com_houde_ffmpeg_test_MyVideoView_play(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);
}

3.在Activity的布局中引入这个MyVideoView,并在代码中调用这个MyVideoView的play方法就可以播放视频了。
布局文件代码:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent" xmlns:app="http://schemas.android.com/apk/res-auto"
        tools:context=".ThirdActivity">
    <com.houde.ffmpeg.test.MyVideoView
            android:layout_width="match_parent"
            android:layout_height="300dp"
            android:id="@+id/surface_view"
            android:layout_centerInParent="true"
    />

    <Button
            app:layout_constraintTop_toBottomOf="@+id/surface_view"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="播放"
            android:onClick="play"
    />

</android.support.constraint.ConstraintLayout>

Activity代码:

open class ThirdActivity : AppCompatActivity() {
    private val inputFilePath = "/storage/emulated/0/GreenCheng/video/123.mp4"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_third)
    }

    fun play(view: View) {
        surface_view.startPlay(inputFilePath)
    }
}

这样整个项目就完成了,视频就能播放了。

源码地址:https://github.com/qq245425070/ffmpeg_play_test

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值