使用FFmpeg实现最简单的视频播放

news2024/12/26 20:52:49

按照之前的编译步骤我们会编译得到使用ffmpeg需要的文件,现在就使用ffmpeg实现最简单的视频播放

集成ffmpeg

  1. 使用Android Studio创建一个Native C++项目
  2. 编译之后得到三个文件夹

include 文件夹放到cpp目录下面。

main 目录下面新建jniLibs 目录把lib文件下的so文件都放进去。

  1. build.gradle 配置
defaultConfig {
    ...
    externalNativeBuild {
        ndk {
            abiFilters "arm64-v8a"
        }
    }
}
...

sourceSets {
    main {
        jniLibs.srcDirs = ['libs']
    }
}
  1. CMake 配置

cmake_minimum_required(VERSION 3.22.1)

project("androidvideoplayer")

# 定义目录名称
set(ffmpeg_lib_dir ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI})
set(ffmpeg_head_dir ${CMAKE_SOURCE_DIR})

# 引入FFmpeg头文件
include_directories(${ffmpeg_head_dir}/include)

# 添加ffmpeg相关的so库
add_library( avutil
        SHARED
        IMPORTED )
set_target_properties( avutil
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libavutil.so )

add_library(
        # 生成的库的名字
        swresample
        # 动态库
        SHARED
        # 源文件
        IMPORTED )
set_target_properties( swresample
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libswresample.so )

add_library(
        avcodec
        SHARED
        IMPORTED )
set_target_properties(
        avcodec
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libavcodec.so )

add_library(
        avfilter
        SHARED
        IMPORTED)
set_target_properties(
        avfilter
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libavfilter.so )

add_library(
        swscale
        SHARED
        IMPORTED)
set_target_properties(
        swscale
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libswscale.so )

add_library(
        avformat
        SHARED
        IMPORTED)
set_target_properties(
        avformat
        PROPERTIES IMPORTED_LOCATION
        ${ffmpeg_lib_dir}/libavformat.so )

add_library( # 生成so的名称
        androidvideoplayer

        SHARED

        # 自己写的c文件,记得每次创建文件要在这里声明,不然无法调用其他库
        ffplayer/logger.h
        ffplayer/ffplayer.cpp
        ffplayer/ffplayer.h
        native-lib.cpp)


find_library(
        log-lib
        log)

target_link_libraries(
        androidvideoplayer
        -landroid

        ## 连接 FFmpeg 相关的库
        avutil
        swscale
        avcodec
        avfilter
        swresample
        avformat
        ${log-lib})

Android 定义接口方法和布局

companion object {
    // 加载so库
    init {
        System.loadLibrary("androidvideoplayer")
    }
}

// 定义接口方法
external fun play(videoPath: String, surface: Surface): Int

布局使用SurfaceView

<LinearLayout 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"
    android:orientation="vertical">

    <SurfaceView
        android:id="@+id/surface_view"
        android:layout_width="match_parent"
        android:layout_height="200dp" />
</LinearLayout>

设置SurfaceView

binding.surfaceView.holder.addCallback(object : SurfaceHolder.Callback {
    override fun surfaceCreated(holder: SurfaceHolder) {
        lifecycleScope.launch(Dispatchers.IO) {
            play("${filesDir}/private.mp4", binding.surfaceView.holder.surface)
        }
    }

    override fun surfaceChanged(
        holder: SurfaceHolder,
        format: Int,
        width: Int,
        height: Int
    ) {
        
    }

    override fun surfaceDestroyed(holder: SurfaceHolder) {
        
    }
}) 

我这里直接在data/data/{packageName}/files/ 下面放了一个MP4格式的视频。

调用ffmpeg 进行视频解析

新建日志工具

#ifndef ANDROIDVIDEOPLAYER_LOGGER_H
#define ANDROIDVIDEOPLAYER_LOGGER_H

#ifdef ANDROID

#include <android/log.h>

#define LOG_TAG    "AndroidVideoPlayer"
#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__)
#define LOGI(format, ...)  __android_log_print(ANDROID_LOG_INFO,  LOG_TAG, format, ##__VA_ARGS__)

#else

#define LOGE(format, ...)  printf(LOG_TAG format "\n", ##__VA_ARGS__)
#define LOGD(format, ...)  printf(LOG_TAG format "\n", ##__VA_ARGS__)
#define LOGI(format, ...)  printf(LOG_TAG format "\n", ##__VA_ARGS__)

#endif

#endif //ANDROIDVIDEOPLAYER_LOGGER_H

ffplayer.h 头文件

#ifndef ANDROIDVIDEOPLAYER_FFPLAYER_H
#define ANDROIDVIDEOPLAYER_FFPLAYER_H
#ifdef __cplusplus
extern "C" {
#endif

#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
#include "libavfilter/avfilter.h"
#include "libavutil/log.h"
#include <libavutil/imgutils.h>
#include <libswscale/swscale.h>
#include "libswresample/swresample.h"
#include "libavutil/time.h"
#include <unistd.h>
#include <android/native_window.h>
#include <android/native_window_jni.h>

#ifdef __cplusplus
}
#endif

class FFplayer {

private:
    int width = 0;
    int height = 0;

    int bufferSize = 0;
    int videoIndex = -1;

    AVFormatContext *avFormatContext = NULL;
    AVCodec *avCodec = NULL;
    AVCodecContext *avCodecContext = NULL;

    AVPacket *avPacket = NULL;
    AVFrame *avFrame = NULL;
    AVFrame *avFrameRGB = NULL;

    SwsContext *swsContext = NULL;
    uint8_t *out_buffer = NULL;
    ANativeWindow_Buffer windowBuffer;

public:
    /**
     * native 播放
     * @param video_path 
     * @param nativeWindow 
     * @return 
     */
    int native_play(const char *video_path, ANativeWindow *nativeWindow);
};


#endif //ANDROIDVIDEOPLAYER_FFPLAYER_H

ffplayer.cpp

#include <jni.h>
#include <cstdio>

#include "logger.h"
#include "ffplayer.h"

int FFplayer::native_play(const char *video_path, ANativeWindow *nativeWindow) {
    int ret = 0;
    int64_t stime = av_gettime_relative();
    LOGD("StartTime: %ld", stime);

    // 创建avFormatContext 上下文
    avFormatContext = avformat_alloc_context();
    // 打开输入文件
    if (avformat_open_input(&avFormatContext, video_path, nullptr, nullptr) != 0) {
        LOGE("Could not open input stream");
        goto finish;
    }
    // 查找文件流信息
    if (avformat_find_stream_info(avFormatContext, nullptr) < 0) {
        LOGE("Could not find stream information");
        goto finish;
    }
    // 查找视频轨道
    for (int index = 0; index < avFormatContext->nb_streams; index++) {
        if (avFormatContext->streams[index]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
            videoIndex = index;
            break;
        }
    }
    if (videoIndex == -1) {
        LOGE("Could not find a video stream");
        goto finish;
    }
    // 查找解码器
    avCodec = const_cast<AVCodec *>(avcodec_find_decoder(avFormatContext->streams[videoIndex]->codecpar->codec_id));
    if (avCodec == nullptr) {
        LOGE("could not find codec");
        goto finish;
    }

    // 创建分配解码器上下文
    avCodecContext = avcodec_alloc_context3(avCodec);
    // 复制解码器参数到解码器上下文
    avcodec_parameters_to_context(avCodecContext, avFormatContext->streams[videoIndex]->codecpar);
    // 打开解码器
    ret = avcodec_open2(avCodecContext, avCodec, nullptr);

    if (ret < 0){
        LOGE("Could not open codec");
        goto finish;
    }
    // 视频宽高
    width = avCodecContext->width;
    height = avCodecContext->height;

    LOGD("ScreenInfo:width-> %d, height-> %d", width, height);

    // 分配 frame 和packet 空间
    avFrame = av_frame_alloc();
    avPacket = (AVPacket *)av_malloc(sizeof(AVPacket));
    avFrameRGB = av_frame_alloc();

    // 绑定输出buffer
    bufferSize = av_image_get_buffer_size(AV_PIX_FMT_RGBA, width, height, 1);
    out_buffer = (uint8_t *)av_malloc(bufferSize * sizeof(uint8_t));

    av_image_fill_arrays(avFrameRGB->data, avFrameRGB->linesize, out_buffer, AV_PIX_FMT_RGBA, width, height, 1);

    // 图像格式转换上下文
    swsContext = sws_getContext(width,
                                height,
                                avCodecContext->pix_fmt,
                                width,
                                height,
                                AV_PIX_FMT_RGBA, SWS_BICUBIC, nullptr, nullptr, nullptr);
    // 修改 windowBuffer 大小 和 像素格式 (YUV -> RGB)
    if (ANativeWindow_setBuffersGeometry(nativeWindow, width, height, WINDOW_FORMAT_RGBA_8888) < 0){
        LOGE("Could not set buffers geometry");
        ANativeWindow_release(nativeWindow);
        goto finish;
    }
    // 循环读Packet
    while (av_read_frame(avFormatContext, avPacket) >= 0){
        if (avPacket->stream_index == videoIndex){
            // 把packet 传进 解码器
            if (avcodec_send_packet(avCodecContext, avPacket) != 0) {
                return -1;
            }
            // 获取 解码后的图像帧
            while (avcodec_receive_frame(avCodecContext, avFrame) == 0){
                // 转化格式 把avFrame 转化到 avFrameRGB
                sws_scale(swsContext,
                          (const uint8_t *const *) avFrame->data,
                          avFrame->linesize,
                          0,
                          avCodecContext->height,
                          avFrameRGB->data,
                          avFrameRGB->linesize);

                // 锁住 window下一个surface
                if (ANativeWindow_lock(nativeWindow, &windowBuffer, nullptr) < 0){
                    LOGE("cannot lock window");
                } else {
                    // 逐行复制  avFrameRGB -> windowBuffer out_buffer
                    auto *bufferBits = (uint8_t *) windowBuffer.bits;
                    for (int h = 0; h < height; h++) {
                        memcpy(bufferBits + h * windowBuffer.stride * 4,
                               out_buffer + h * avFrameRGB->linesize[0],
                               avFrameRGB->linesize[0]);
                    }

                    // 解锁 window的surface 并post去显示
                    ANativeWindow_unlockAndPost(nativeWindow);
                }

                // 解码速度大于正常的播放速度处理;使画面刷新频率为正常值
                int64_t pts = avFrame->pts;
                av_frame_unref(avFrame);
                AVRational time_base = avFormatContext->streams[videoIndex]->time_base;
                double timestamp = (double)pts * av_q2d(time_base);
                int64_t master_clock = av_gettime_relative() - stime;
                int64_t diff = (int64_t)(timestamp * 1000 * 1000) - master_clock; // microseconds
                if (diff > 0) {
                    usleep(diff);
                }
            }
        }
        av_packet_unref(avPacket);
    }

    // 释放内存
    sws_freeContext(swsContext);
    finish:
        av_free(avPacket);
        av_free(avFrameRGB);
        avcodec_close(avCodecContext);
        avcodec_free_context(&avCodecContext);
        avformat_close_input(&avFormatContext);
    return 0;
}

native-lib.cpp

新建的项目中本来就有个stringFromJNI,我们直接新建一个方法,AS输入函数名称会自动生成整个函数,灰常的方便,当然也可以使用动态注册函数的方式,这样更方便统一管理。

#include <jni.h>
#include <string>
#include "ffplayer/ffplayer.h"
#include "ffplayer/logger.h"

extern "C"
JNIEXPORT jint JNICALL
Java_com_hsw_androidvideoplayer_FFPlayerActivity_play(JNIEnv *env, jobject thiz, jstring videoPath, jobject surface) {
    const char *video_path = env->GetStringUTFChars(videoPath, nullptr);
    // 创建nativeWindow
    ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
    if (window == nullptr) {
        LOGE("Could not get native window from surface");
        return -1;
    }
    // 封装类
    FFplayer nativePlayer ;// = NativePlayer()
    nativePlayer.native_play(video_path, window);

    env->ReleaseStringUTFChars(videoPath, video_path);
    return 0;
}

现在我们就实现了一个最基础的视频播放器。

这里我们只是熟悉下ffmpeg的api,了解播放一个视频需要什么api,api的功能是什么。

播放功能还很不健全,还有点BUG,后面会持续补充功能:

TODO

  • 暂停、停止
  • 倍速播放
  • GIF生成、截屏功能
  • 添加水印
  • 播放rtmp、hls流

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/616992.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Java之BigDecimal使用

Java之BigDecimal使用 1、BigDecimal概述 ​ BigDecimal用来对超过16位有效位的数进行精确的运算。双精度浮点型变量double可以处理16位有效数&#xff0c;但在实际应用中&#xff0c;可能需要对更大或者更小的数进行运算和处理。一般情况下&#xff0c;对于那些不需要准确计…

OA系统,企业数字化转型的重要工具,用现成还是自己搭建呢

什么是OA系统 OA系统是办公自动化系统的简称&#xff0c;它是指一种基于计算机技术的办公工作管理系统&#xff0c;用于协调和规划企业内部各部门的信息发布、通信、人员流动、文档管理等方面的工作。它可以有效地提高企业办公效率和工作效益&#xff0c;优化企业内部沟通协作…

计算机视觉 | 深度学习预训练与MMPretrain

前言 MMPretrain是一款基于pytorch的开源深度学习预训练工具箱&#xff0c;是OenMMLab的项目成员之一。它是一个全新升级的预训练开源算法框架&#xff0c;旨在提供各种强大的预训练主干网络&#xff0c;并支持了不同的预训练策略。 一、MMPretrain算法库介绍 MMPretrain 源…

几分钟上线一个应用,这个神器我爱了!

配置一套公司企业运用的SaaS工作流办公管理系统需要多久&#xff1f;需要多少人才能开发出来&#xff1f;传统软件开发起码需要10个人&#xff0c;花上个把月时间&#xff0c;才能做出一套比较完整的SaaS工作流办公管理系统。 传统的开发模式它需要前后端程序员以及各平台系统的…

【Docker】浅谈Docker之AUFS、BTRFS、ZFS、Container、分层的概念

作者简介&#xff1a; 辭七七&#xff0c;目前大一&#xff0c;正在学习C/C&#xff0c;Java&#xff0c;Python等 作者主页&#xff1a; 七七的个人主页 文章收录专栏&#xff1a; 七七的闲谈 欢迎大家点赞 &#x1f44d; 收藏 ⭐ 加关注哦&#xff01;&#x1f496;&#x1f…

【算法】--- 几分钟了解直接选择排序(排序中最简单的排序)+快排(解决一切的优质算法)(中)

文章目录 前言&#x1f31f;一、常见的排序算法&#xff1a;&#x1f31f;二、选择排序---直接选择排序&#xff1a;&#x1f30f;2.1.1 基本思想&#xff1a;&#x1f30f;2.1.2 直接选择排序:&#x1f30f;2.1.3 直接选择排序的特性总结&#xff1a;&#x1f30f;2.1.4 思路&…

Vue3 Vite4 ElementPlus TS模板(含Vue-Router4+Pinia4)

引言 手动安装配置Vue3 ElementPlus模板比较繁琐&#xff0c;网上寻找一些模板不太符合自己预期&#xff0c;因此花点精力搭建一个符合自己需求的架子 采用最新的组件&#xff0c;版本如下&#xff1a; vite 4.3.9vite-plugin-mock 2.9.8vue 3.3.4pinia 2.1.3vue-router 4.2.2…

总结6种服务限流的实现方式

服务限流&#xff0c;是指通过控制请求的速率或次数来达到保护服务的目的&#xff0c;在微服务中&#xff0c;我们通常会将它和熔断、降级搭配在一起使用&#xff0c;来避免瞬时的大量请求对系统造成负荷&#xff0c;来达到保护服务平稳运行的目的。下面就来看一看常见的6种限流…

推荐常用的排序学习算法——BPR(贝叶斯个性化排序)

文章目录 1. 排序学习1.1 优势1.2 排序学习在推荐领域的作用1.3 排序学习设计思路1.3.1 单点法&#xff08;Pointwise&#xff09;1.3.2 配对法&#xff08;Pairwise&#xff09;1.3.3 列表法&#xff08;Listwise&#xff09; 2. BPR&#xff08;贝叶斯个性化推荐&#xff09;…

投票评选活动小程序的分享功能和背景音乐功能实现

投票评选活动小程序的分享功能和背景音乐功能实现 投票评选活动过程中&#xff0c;需要转发分享出去&#xff0c;实现投票的效果&#xff0c;那么就需要分享功能&#xff0c;不然怎么实现投票呢&#xff0c;其实这个是最具价值的功能之一。 而背景音乐播放功能&#xff0c;只…

路径规划算法:基于静电放电优化的路径规划算法- 附代码

路径规划算法&#xff1a;基于静电放电优化的路径规划算法- 附代码 文章目录 路径规划算法&#xff1a;基于静电放电优化的路径规划算法- 附代码1.算法原理1.1 环境设定1.2 约束条件1.3 适应度函数 2.算法结果3.MATLAB代码4.参考文献 摘要&#xff1a;本文主要介绍利用智能优化…

Qt/GUI/布局/实现窗口折叠效果/且在操作时父窗口尺寸跟随变动

文章目录 概述无法resize到小尺寸可行方案其他方案 概述 本文旨在&#xff0c;实现如下所示的显示或隐藏 ‘附加选项’ 的效果&#xff0c;以折的不常用信息和操作项&#xff0c;减少普通用户负担&#xff0c;提升用户体验。在某些软件中此类窗口折叠效果&#xff0c;常用 “……

SpringCloud断路器

SpringCloud断路器 Hystrix 简介 hystrix对应的中文名字是“豪猪”&#xff0c;豪猪周身长满了刺&#xff0c;能保护自己不受天敌的伤害&#xff0c;代表了一种防御机制。 这与hystrix本身的功能不谋而合&#xff0c;因此Netflix团队将该框架命名为Hystrix&#xff0c;并使用…

2023最详细的接口测试用例设计教程,详细文档等你来拿

目录 一、接口测试流程 二、分析接口文档元素 三、如何设计接口测试用例 四、常用的接口测试用例覆盖方法 五、接口测试接口优先级 六、接口测试的设计思路分析 七、接口测试返回结果的比较 一、接口测试流程 1、需求讨论 2、需求评审 3、场景设计 4、数据准备 5、测试执…

sdf与timingCheck和后仿真

目录 1.Distributed delays 2.specify--endspecify 1.1 specify内部语法 2.sdf 2.1 sdf的格式 3.timingCheck和网表后仿真 4.关于负值delay sdf和 module 里面的specify--endspecify都可以对路径延时进行赋值和检查&#xff1b;HDL语言中的‘#()’也可以描述延时【叫做D…

没事千万别动生产服数据库 - 来自小菜鸟的忠告

阿里云官方参考文档 目录 背景一、环境部署二、目录规划三、操作步骤FAQ 背景 今天把一张 5500 多万条记录的表进行按年度拆分&#xff0c;本来打算将表数据拆分为 2020 年、2021 年、2022 年三张新表&#xff0c;提升原表查询效率&#xff0c;仅保留 2023 年数据。表拆分完毕…

【SpinalHDL快速入门】4.1、基本类型之Bool

Tips1&#xff1a; 由于SpinalHDL是基于Scala构建的&#xff0c;Scala本身自带类似变量Boolean&#xff0c;故在此要认准SpinalHDL中采用的是Bool而非Boolean&#xff1a; Bool&#xff08;大写的True和False&#xff09;&#xff1a;True表示1&#xff0c;False表示0Boolean&…

Vue3搭建

Vue3项目搭建全过程 vue create 项目名 选择手动吗&#xff0c;自定义安装 选择vue3 是否选择class风格组件 选择ts处理工具和css预处理器 Y 是否使用router的history模式 Y 选择css预处理语言 ;less 9.选择lint的检查规范 只使用EsLint官网推荐规范 使用EsLint官网推荐规…

MyBatis-plus(1)

基本概念: 一)开发效率也就是我们使用这款框架开发的速度快不快&#xff0c;是否简单好用易上手。从这个角度思考&#xff0c;每当我们需要编写一个SQL需求的时候&#xff0c;我们需要做几步 1)Mapper接口提供一个抽象方法 2)Mapper接口对应的映射配置文件提供对应的标签和SQL语…

论文笔记--LLaMA: Open and Efficient Foundation Language Models

论文笔记--LLaMA: Open and Efficient Foundation Language Models 1. 文章简介2. 文章概括3 文章重点技术3.1 数据集3.2 模型训练 4. 数值实验5. 文章亮点6. 原文传送门7. References 1. 文章简介 标题&#xff1a;LLaMA: Open and Efficient Foundation Language Models作者…