react-native 集成 FFmpeg 能力 - 使用 FFmpegKit(三)

相关文章:
react-native 集成 FFmpeg 能力 - 使用 FFmpegKit(一)
react-native 集成 FFmpeg 能力 - 使用 FFmpegKit(二)

这一篇主要记录自己尝试打包 FFmpegKit 遇到的一些问题

FFmpegKit 仓库地址

网上可以找到一些大佬打好的全功能包,但是有些功能可能用不上,自己不需要的话,可以尝试自己本地编译一个。
仓库作者大佬也把怎么打写在文档里了,包括多平台的。这里我只尝试打包 apple 和 android 的。

0、

首先最后一步是需要 x86 架构才能打出来,如果是苹果芯片电脑,需要安装 Rosetta 工具。否则会报这样的错:
arch: posix_spawnp: zsh: Bad CPU type in executable

先查看自己电脑有没有安装:

pgrep -q oahd && echo "Rosetta 已安装" || echo "Rosetta 未安装"

没有的话就进行安装

softwareupdate --install-rosetta

接着安装文档安装一系列必备的依赖和一些环境变量的设置,比如 android 需要设置 sdk 和 ndk 的路径。

其他问题:

1、

可能会遇到需要升级 bison 的问题,电脑自带的可能才 2.3,可以用 homebrew 下载新的版本,然后覆盖电脑自带的
相关 issue 《fails to build with –enable-gnutls》。

2、

可能会遇到 cmake 最低版本要求,报错:CMake Error at CMakeLists.txt:29 (cmake_minimum_required):
去相应的 CMakeLists.txt 文件,把 cmake_minimum_required (VERSION 3.1 FATAL_ERROR) 这一句的那个 3.1 数据改成 3.5

3、

用 —full 的时候,可能最后打包会报错,openssl 和 gnutls 不能同时启动。所以改成不要用 —full,按官网 readme 把所需要的参数一个个 enable 进去。二选一,deepseek 推荐用 openssl

4、

Android ndk 版本最好用 25.1.8937393,其他版本我试了都报错。

5、

报了一个缺失库的错误,记不太清楚了。是有一份文件有关于头文件引入的判断,大概是判断要引入类似于这样的库: <f.h> 还是 <math.h>,然后条件判断是要引入 <f.h>。结果这个库比较老导致报错了。我就尝试都改成使用 <math.h> 就通过了。具体是哪一份文件的哪一行记不住了。
本来文章就是想记录这个,结果却忘了。= =!

react-native 集成 FFmpeg 能力 - 使用 FFmpegKit(二)

相关文章:
react-native 集成 FFmpeg 能力 - 使用 FFmpegKit(一)
react-native 集成 FFmpeg 能力 - 使用 FFmpegKit(三)

这一篇主要记录对于 react-native 项目里,直接使用原生模块。

其实 react-native 能用起来,那就意味着原生的引入已经成功了。但是我以为和上一篇处理 react-native 库一样,在根目录的 ios 和 android 文件夹引入声明,相当于全局声明,自定义原生模块就可以直接使用了,但实际上不是,还需要在相应的自定义原生模块里再引入声明。

expo 项目有可以协助创建自定义原生模块的 create-expo-module。然后前提还是先把 ffmpegkit 的库下到本地来引入。

1. expo 项目 Android 引入

modules/native-module/android/build.gradle 加上:

dependencies {
    // 1. 直接引入 AAR,位置合理放置就行
    implementation files('libs/ffmpeg-kit-full-6.0-2.aar')
    // 2. 添加传递依赖(必须)
    implementation 'com.arthenica:smart-exception-java:0.2.1'
}

在代码里使用:

package expo.modules.settings

// 引入下面这三个
import com.arthenica.ffmpegkit.FFmpegKit
import com.arthenica.ffmpegkit.FFmpegSession
import com.arthenica.ffmpegkit.ReturnCode

import expo.modules.kotlin.modules.Module
import expo.modules.kotlin.modules.ModuleDefinition

class ExpoSettingsModule : Module() {
  override fun definition() = ModuleDefinition {
    Name("ExpoSettings")

    Function("getTheme") {
      return@Function "system"
    }
  }
}

2. expo 项目 IOS 引入

modules/native-module/ios/NativeModule.podspec 加上:

Pod::Spec.new do |s|
  s.name           = 'NativeModule'
  s.version        = '1.0.0'
  s.summary        = ''
  s.description    = ''
  s.author         = ''
  s.homepage       = 'https://docs.expo.dev/modules/'
  s.platforms      = {
    :ios => '15.1',
    :tvos => '15.1'
  }
  s.source         = { git: '' }
  s.static_framework = true

  s.dependency 'ExpoModulesCore'

  # Swift/Objective-C compatibility
  s.pod_target_xcconfig = {
    'DEFINES_MODULE' => 'YES',
  }

  s.source_files = "src/**/*.{h,m,mm,swift,hpp,cpp}"
  // 直接引入 xcframework, 位置合理放置就行
  s.vendored_frameworks = 'libs/*.xcframework'
end

在代码里使用:

import ExpoModulesCore

// 加上这句引用
import ffmpegkit

public class ExpoSettingsModule: Module {
  public func definition() -> ModuleDefinition {
    Name("ExpoSettings")

    Function("getTheme") { () -> String in
      "system"
    }
  }
}

下面两个只是尝试,然后记录一下,实际上用的上面 expo 项目

3. react-native 项目 IOS 引入, 使用 swift 语法

本质上差别不大,只是没有 expo 的辅助,改回 react-native 要求的语法。

modules/native-module/src/NativeModule.ts 改成:

// expo 改成 react-native 的模块引入方式
import { NativeModules } from 'react-native';
export const NewVideoEditor = NativeModules.NewVideoEditorModule;

podspec 文件和 expo 项目一样就行。

新增 modules/new-module/ios/src/NativeModule.m 文件:

#import <React/RCTBridgeModule.h>
#import <FFmpegKit/FFmpegKit.h>

@interface RCT_EXTERN_MODULE(NewVideoEditorModule, NSObject)

// 同步方法 - getName
RCT_EXTERN_METHOD(getName)

// 异步方法 - getFFmpegVersion
RCT_EXTERN_METHOD(getFFmpegVersion:(RCTPromiseResolveBlock)resolve
                  rejecter:(RCTPromiseRejectBlock)reject)

@end

新增 modules/new-module/ios/src/NativeModule.swift 文件:

import Foundation
import React
import ffmpegkit

@objc(NewVideoEditorModule)
class NewVideoEditorModule: NSObject, RCTBridgeModule {
    // MARK: - RCTBridgeModule
    static func moduleName() -> String! {
        return "NewVideoEditorModule"
    }
    static func requiresMainQueueSetup() -> Bool {
        return false
    }
    // MARK: - Exported Methods
    // 导出同步方法 - 使用 @objc 标记,React Native 会自动发现
    @objc
    func getName() -> String {
        return "NewVideoEditor-Name"
    }
    @objc
    func getFFmpegVersion(_ resolve: @escaping RCTPromiseResolveBlock,
                         rejecter reject: @escaping RCTPromiseRejectBlock) {
        let version = FFmpegKitConfig.getFFmpegVersion()
        resolve(version)
    }
}

上面的代码也是问 AI 问出来的,仅供参考,模块名和文件名需要按实际填写。这里的注意点还是原生 ios 的问题吧,据说 ios 提供给 react-native 的库是用 Objective-c 写的。所以在 react-native 里自定义原生 ios 模块里写 swift 也要包装成 Objective-c。

4. react-native 项目 IOS 引入, 使用 Objective-c 语法

参考 FFmpegKit 仓库 的写法就行,就是用 Objective-c 写的。
ios 的声明文件 podspec 文件和 ts 的导出文件 NativeModule.ts 和 swift 写法的一样就行。

react-native 集成 FFmpeg 能力 - 使用 FFmpegKit(一)

相关文章:
react-native 集成 FFmpeg 能力 - 使用 FFmpegKit(二)
react-native 集成 FFmpeg 能力 - 使用 FFmpegKit(三)

这一篇主要记录对于 react-native 库的使用。

最近在做 react-native 的需求,需要做一些视频剪辑。ios 上使用的是 AVFoundation,询问了 AI 还比较顺利,功能实现了。Android 上使用的是 MediaCodec,就是怎么都调试不出来。AI 也直接建议用 FFmpeg 了。

一、现状调研

FFmpeg 也是个很知名很强大的音视频处理工具了,一般用在命令行。在这个基础上,网上有大佬封装适配了多个平台的库,其中也包括 react-native,仓库地址 ffmpeg-kit。很遗憾的是,今年年初因为人力资源问题,大佬停止了更新。

这里提到了:《Saying Goodbye to FFmpegKit》。感谢大佬们的用爱发电啊。

截止到当前,FFmpeg 版本是 7,FFmpegKit 封装的版本是 6。同时 FFmpegKit 也删除了 IOS 和 Android 仓库里的库,所以 react-native 的也用不了了。但是仓库源码还在。所以目前还想继续使用 FFmpegKit 的话,可以:

  1. 找之前网上保留过的资源,或者其他大佬私下打包出来分享的。
  2. 自己根据源码打包,因为不维护了,自己解决打包出现的问题。

二、资源准备

当然找到了资源包,也得自己引入,因为正常的安装流程已经报错了,得本地引入(自己打包放到网上仓库也可以),首先:

Android - aar 下载,然后在本地引入。
IOS - 参考文章,可以将 podFile 指向 FFmpegKit 原仓库的地址改成指向「别的地址」(其实就是这个文章作者大佬自己的仓库),保证能下载到依赖压缩包就行,也可以根据这份 podspec 文件里面指向的 zip 包地址,把依赖包下载下来,放在自己本地引入。

三、react-native 库引入使用

react-native 库:《ffmpeg-kit-react-native》,原《使用文档》,现在安装这个库会报已经废弃的警告。

现在在获得 IOS 和 Android 依赖包的情况下,进行本地引入,使得 react-native 库也能用起来。当然相应的 IOS 和 Android 库也能用。
首先在项目根目录放置两个依赖库(地址随意,用的时候正确使用就行):

ios 文件夹有一个 ffmpeg-kit-ios-video.podspec 文件,这不是资源包下载下来附带的,要自己添加,相当于依赖声明。

Android 端修复使用

Android 端的使用还是比较简单的,直接打 patch 修改 node_modules 里的文件内容就行:找到 node_modules/ffmpeg-kit-react-native/android/build.gradle 文件,修改成一下这样即可:

dependencies {
  api 'com.facebook.react:react-native:+'
  // 这句注释:implementation 'com.arthenica:ffmpeg-kit-' + safePackageName(safeExtGet('ffmpegKitPackage', 'https')) + ':' + safePackageVersion(safeExtGet('ffmpegKitPackage', 'https'))
  // 添加下面两句,引入本地依赖和其他依赖
  implementation files('../../../libs/android/ffmpeg-kit.aar')
  implementation 'com.arthenica:smart-exception-java:0.2.1'
}

IOS 端修复使用

1、

首先上面提到的 ffmpeg-kit-ios-video.podspec

Pod::Spec.new do |s|
  s.name         = 'ffmpeg-kit-ios-video'
  s.version      = '6.0'
  s.summary      = 'FFmpeg Kit for React Native'
  s.homepage     = 'https://github.com/arthenica/ffmpeg-kit'
  s.license      = 'LGPL-3.0'
  s.authors      = 'author'
  s.platform          = :ios
  s.requires_arc      = true
  s.static_framework  = true
  s.source       = { path: '.' }
  s.dependency "React-Core"
  s.vendored_frameworks = '*.xcframework'
end
2、

然后项目下找到 ios/Podfile 文件,修改:

// 备用:pod 'ffmpeg-kit-ios-full', :podspec => 'https://raw.githubusercontent.com/luthviar/ffmpeg-kit-ios-full/main/ffmpeg-kit-ios-full.podspec'
pod 'ffmpeg-kit-ios-video', :path => '../libs/ios/ffmpeg-kit-ios-video'
pod 'ffmpeg-kit-react-native', :subspecs => ['video'], :podspec => '../node_modules/ffmpeg-kit-react-native/ffmpeg-kit-react-native.podspec'
// 找到下面这一句,然后在这一句上面添加上面那两句,顺序严格
config = use_native_modules!(config_command)

注意:这里的依赖库名是 ffmpeg-kit-ios-video,是因为我自己本地打了个 video 包出来,所以命名为 video 后缀。如果自己打出来了别的包,或者网上找的包是 full 或者别的名字,只要把所有相应的名字改了就行。

比如 ffmpeg-kit-ios-video 全改成 ffmpeg-kit-ios-full,包括那个 libs/ios 下的 podspec 文件。
:subspecs => ['video'] 理论上应该也没影响了,不过也最好改一下,改成::subspecs => ['full']

3、

如果是 expo 项目,这里有个小问题。ios 文件夹都是每次 build 生成的,当然里面的 Podfile 文件也是,也就是每次重新创建文件,都要自己去打这个补丁。太麻烦了,所以得换另外方式。
注意下面示例我是用了 ffmpeg-kit-ios-full 的包,这个和自己本地引入的实际包名保持一致就好了。

用官方的工具 expo-build-properties,在 app.config.ts 文件加上:

const config = {
  // 其他省略
  plugins: [
    [
      'expo-build-properties',
      {
        ios: {
          extraPods: [
            {
              name: 'ffmpeg-kit-ios-full',
              path: '../libs/ios/ffmpeg-kit-ios-full', // 本地路径(相对于 ios/Podfile)
            },
            {
              name: 'ffmpeg-kit-react-native',
              podspec:
                  '../node_modules/ffmpeg-kit-react-native/ffmpeg-kit-react-native.podspec', // 本地路径(相对于 ios/Podfile)
            },
          ],
        },
      },
    ],
  ],
}

这样,每次 build 创建 ios/Podfile 文件时候,就会打上上面的补丁。可以看到上面是打了两份补丁,一个 ffmpeg-kit 本身,自己使用本地引入的。一个是对应的 react-native 库,指向了 node_modules 里的文件,但是这个 react-native 库 podspec 文件已经知道是有问题的。
所以还要再对 node_modules 的 ffmpeg-kit-react-native 依赖包再打一份针对 ios 的 patch:

Pod::Spec.new do |s|
  s.source       = { :git => "https://github.com/arthenica/ffmpeg-kit.git", :tag => "react.native.v#{s.version}" }
  s.dependency "React-Core"
  s.source_files      = '**/FFmpegKitReactNativeModule.m',
    '**/FFmpegKitReactNativeModule.h'
  s.dependency 'ffmpeg-kit-ios-full', "6.0"
  s.ios.deployment_target = '10'
end

四、测试使用

本地库和配置文件都准备完毕,因为改了原生依赖,所以最好重新编译一遍。我使用的是 expo 项目,可以执行:npx expo prebuild --clean 重新安装一遍。
如果用的纯 react-native 项目,就用 xcode 和 Android Studio 重新安装。

在代码里测试是否引用成功:

import { ReturnCode } from 'ffmpeg-kit-react-native';
FFmpegKitConfig.getFFmpegVersion().then(v => console.log('getFFmpegVersion', v))

react 使用 swr 做数据请求

官网介绍:用于数据请求的 React Hooks 库

一开始想着不就是把请求变成 hook 写法来用吗?就是多了一个方便 loading 标志位的处理。抱着试一试的心态用了,用着用着感觉还挺不错,记录下使用方法。

一、基础用法

在一些进入页面就要请求的场景里,比如列表页,配置项什么的,特别合适。传统的写法可能是 useEffect(() => {}, []) 里触发一下接口请求。用了 swr 后就可以不用这么写了,官网示例:

import useSWR from 'swr'
 
function Profile() {
  const { data, error, isLoading, mutate } = useSWR('/api/user', fetcher)
 
  if (error) return <div>failed to load</div>
  if (isLoading) return <div>loading...</div>
  return <div>hello {data.name}!</div>
}

在传统场景可能就需要声明一个 loading 变量,然后自行管理,先 setLoading(true) 又 setLoading(false)。同理,data 也得用 useState 处理一下。这里直接把数据、错误,等待标志位,摊平来使用。页面一多,还是便利不少的,特别是 B 端的列表页面。在编辑完信息后,需要更新内容可以主动调用一次 mutate 来触发一次请求。

二、使用场景

众所周知,hook 不能写在条件里,所以这个很适合写在一开始必须请求的场景。当然 swr 也是预留了这个场景,如果开始传入的是空路径,就不会请求。也是官网示例:条件数据请求

function Profile () {
  const { data } = useSWR(isReady ? '/api/user' : null, fetcher, { suspense: true })
 
  // 如果 `isReady` 是 false,`data` 将会是 `undefined`
  // ...
}

这个可以在需要有前置条件的时候,比如第二个接口依赖于第一个接口返回的某个字段,这时候就可以先这样按住第二个接口。

这个也可以用于用户行为后的请求,比如按钮点击后的,登录什么的。但我觉得这种场景下,用 swr 就很别扭。而且一般都会把接口声明在一个地方,方便维护。那么此时这两种场景下的接口写法就不一样,会出现一部分接口一定是夹杂了条件判断。

所以,我认为接口还是按照原来的声明,只是在列表之类的接口,要使用的时候再看情况用 swr 包装一层。如:

const getUser = () => fetcher('/api/user')

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', () => getUser())
}

三、携带参数

接口免不了要携带参数,比如列表搜索也有一堆过滤参数,可以这样写:

import useSWR from 'swr'
 
function Profile() {
  const [formData] = useState({ a: 1 })
  const { data, error, isLoading } = useSWR(['/api/user', formData], ([, params]) => fetcher(params))
}

// 不要写成下面这种
// function Profile() {
//   const [formData] = useState({ a: 1 })
//   const { data, error, isLoading } = useSWR('/api/user', () => fetcher(formData))
// }

这样可以达到修改参数 formData 的时候,就会自动触发带上新的参数一次请求。**注意不要写成上面第二种,这样参数相当于写在闭包里,再次请求会变成携带旧的参数。

四、工具参数

有一些很实用的参数:选项,这里列举几个改到默认配置的:

  • keepPreviousData 改为 true,默认为 false,在列表重新请求时会把数据先置空,这时列表就白了。改为 true 后就是数据来了再更新,不会有这个闪的效果。
  • revalidateOnFocus 改为 false,默认为 true,在切换 tab 的时候,回到本页面也就是页面 focus 事件触发一次请求,可能大部分场景不需要这样。
  • focusThrottleInterval 如果需要聚焦时再度请求,可以搭配这个限流一下。
  • refreshInterval 这个就顾名思义了,开启轮询。

可以单个使用:

const { data } = useSWR('/api/user', fetcher, { keepPreviousData: true })

每个都写太麻烦,可以全局配置,然后个别灵活处理:

<SWRConfig value={{ keepPreviousData: true }}>
  <Page />
</SWRConfig>

chrome 插件基础模板

一直以来,都觉得 chrome 插件挺方便的,可以做出灵活又好用的工具。工作中也接触过一些插件的开发,但是都没有系统的了解下。这次花点时间记录下。

因为刚好发现这个工具《What is CRXJS》,用 vite 做开发工具,支持前端框架有 react、vue、solid 等。这样一来就省了很多事了,chrome 插件也可以使用漂漂亮亮的组件库了。

具体有《官方文档》,这里记录一下快速使用的点。

工程地址:《chrome-extension-plugin-demo

注意:
1. 最新 chrome 浏览器版本已经禁止的 manifest_version 2 版本的使用了,现在开始都是 3 了。
2. 这个 @crxjs/vite-plugin 工具要使用 beta 版本,latest 在 manifest_version 3 会因为资源安全问题加载报错。

介绍

manifest 的字段为介绍:

{
  "manifest_version": 3,
  "name": "Chrome Extension",
  "version": "1.0.0",
  "description": "Chrome Extension React Vite Example",
  "permissions": ["tabs", "notifications"],
  "action": {
    "default_popup": "src/popup/index.html",
    "default_icon": "src/assets/images/icon.png"
  },
  "icons": {
    "16": "src/assets/images/icon-16.png",
    "32": "src/assets/images/icon-32.png",
    "48": "src/assets/images/icon-48.png",
    "128": "src/assets/images/icon-128.png"
  },
  "background": {
    "service_worker": "src/background/index.ts",
    "type": "module"
  },
  "content_scripts": [
    {
      "js": ["src/scripts/content.ts"],
      "matches": ["<all_urls>"]
    }
  ]
}
  • permissions:声明要开通的模块,不然调用 api 会失败。
  • action:就是右上角插件面板里,那个按钮点击后弹出框的入口文件和显示图标。
  • background:后台运行的代码,这里可以调用的 api 更全一点。可以指定以 es module 模式加载。
  • content_scripts:会嵌入到当前访问网站里,可以读取当前网站的 dom,storage 等,实际就是当前网站的一部分了。matches 可以指定哪些网站可以嵌入。具体可以看《匹配模式》。

示例工程,用了多页面,但是这个 @crxjs/vite-plugin 只默认加载了 action 做入口文件。所以要自己配一下其它入口页面。

import { resolve } from 'node:path';
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { crx, type ManifestV3Export } from '@crxjs/vite-plugin';
import manifest from './manifest.json';

export default defineConfig({
  plugins: [react(), crx({ manifest: manifest as ManifestV3Export })],
  build: {
    rollupOptions: {
      input: {
        main: resolve(__dirname, 'src/main/index.html'),
      },
    },
  },
});

开发

像往常一样启动 vite 工程就行,只不过配置了开发时也把文件存在硬盘,因为要丢到插件库里去运行。执行 pnpm dev 后就会产出 dist 文件夹,浏览器打开 chrome://extensions/,然后把整个 dist 文件夹往浏览器界面拖就行。当然右上角的开发者模式要打开。
chrome://extensions 页面 “所有扩展程序” 下,有自己的开发程序那就是成功了。大部分代码更新都是可以马上生效不需要刷新加载的。改了配置之类的就得刷新加载,此时点击该插件面板右下角的刷新按钮就行了。