把 Flutter 插件搬上 OpenHarmony:手把手适配音频录制库

把 Flutter 插件搬上 OpenHarmony:手把手适配音频录制库

前言

OpenHarmony(后面简称 OHOS)的生态越来越热闹,它的分布式能力和全场景体验确实给开发带来了新的想象空间。对于我们这些熟悉 Flutter 的开发者来说,很自然会想:能不能把 Flutter 丰富的跨平台生态和 OHOS 的原生能力结合起来?这既能扩大应用的覆盖面,也能提升开发效率。

但想法很美好,现实却有道坎:Flutter 海量的第三方库,绝大多数都是为 Android 和 iOS 准备的,想让它们在 OHOS 上顺利跑起来,是个既关键又充满挑战的技术活儿。

今天,我就以一个具体的音频录制插件 flutter_record_plugin 为例,和大家一起拆解一下 Flutter 插件适配 OHOS 的全过程。通过这个例子,你不仅能学会怎么迁移一个具体的插件,更能掌握一套可以复用的方法和思路,为你后续引入更多 Flutter 生态库铺平道路。

一、 理解适配的核心:原理与技术分析

1.1 Flutter 插件是怎么工作的?

简单来说,Flutter 插件就是一个通信桥梁,核心是 Platform Channel。当 Flutter 层的 Dart 代码通过 MethodChannel 发起调用时,这个消息会被序列化,然后传递到原生平台(Android/iOS),由那边的原生代码执行具体的功能(比如启动录音),最后再把结果传回 Dart 层。

Dart层 (Flutter) <--(Platform Channel)--> 原生平台 (Android/iOS/OHOS)

1.2 为 OHOS 适配,关键要做什么?

适配到 OHOS,核心任务就是为这个通信桥梁在 OHOS 端建造一个新的“桥墩”。OHOS 有自己的一套体系,虽然它的 Ability 框架和 UI 框架在理念上和 Android 有些相似,但 API、权限模型、系统服务调用方式都自成一体。所以,我们的工作主要聚焦于两点:

  1. 接口对齐:在 OHOS 端,原样实现 Flutter 插件约定好的那些 MethodChannel 接口方法。
  2. 能力映射:用 OHOS 原生提供的 API(比如 AudioCapturer、文件操作)去具体实现插件要求的功能(比如录音、存文件)。

1.3 两种适配策略怎么选?

  • 完全重写:如果插件功能复杂,或者和 Android API 绑定得很深,最稳妥的办法就是为 OHOS 单独建立一个原生实现目录(比如 ohos/),从头写起。
  • 部分复用:如果插件的核心业务逻辑比较独立,可以尝试把这部分逻辑抽成公共代码,然后分别写 Android 和 OHOS 的“外壳”来调用它。不过对于初次适配,通常重写更清晰。

二、 实战开始:适配 flutter_record_plugin

2.1 搭好环境,准备开工

首先,确保你的“装备”齐全:

  • Flutter SDK: 3.19.0 或更高(需要支持 OHOS 平台)
  • DevEco Studio: 4.0 或更高(用于 OHOS 原生开发)
  • OHOS SDK: API 12+(对应 HarmonyOS 5.0)
  • Node.js: 18.17.0+

用命令创建一个支持 OHOS 的插件模板,这是我们的起点:

# 1. 创建插件模板
flutter create --platforms=ohos --template=plugin flutter_record_plugin_ohos
cd flutter_record_plugin_ohos

# 2. 看看生成的结构,重点留意 `ohos/` 这个新目录
ls -la

2.2 分析插件,设计结构

原来的 flutter_record_plugin,它的 Dart API 通常提供了这几个方法:

  • startRecording(String path)
  • stopRecording()
  • getAmplitude() (这个不一定所有版本都有)
  • dispose()

我们的目标,就是在 ohos/ 目录下,建立一个能响应这些方法调用的原生实现。

2.3 编写 OHOS 原生代码

2.3.1 声明必要的权限 (module.json5)

在 OHOS 上,权限需要在配置文件中明确声明。

{
  "module": {
    // ... 其他配置
    "requestPermissions": [
      {
        "name": "ohos.permission.MICROPHONE"
      },
      {
        "name": "ohos.permission.WRITE_AUDIO"
      },
      {
        "name": "ohos.permission.READ_AUDIO"
      }
      // 根据是否需要位置信息,决定是否添加 MEDIA_LOCATION
    ]
  }
}
2.3.2 定义核心音频服务接口 (audio_capture_interface.h)

为了让代码结构更清晰,我们先定义一个 C++ 接口,把核心功能和 Platform Channel 的胶水代码分开。

#ifndef AUDIO_CAPTURE_INTERFACE_H
#define AUDIO_CAPTURE_INTERFACE_H

#include <string>

class AudioCaptureInterface {
public:
    virtual ~AudioCaptureInterface() = default;
    virtual bool start(const std::string &filePath) = 0;
    virtual bool stop() = 0;
    virtual double getCurrentAmplitude() = 0; // 用于获取当前音量振幅
    virtual void release() = 0;
    virtual bool isRecording() const = 0;
};

#endif
2.3.3 实现 OHOS 音频录制功能 (ohos_audio_capturer.cpp,关键部分节选)

这里就是真正的业务逻辑了,我们使用 OHOS Native API 中的 AudioCapturer

#include "ohos_audio_capturer.h"
#include <multimedia/player_framework/audio_capturer.h>
#include <fcntl.h>
#include <unistd.h>
#include "hilog/log.h"

// 定义日志标签
#define LOG_TAG "FlutterRecordPlugin"

using namespace OHOS::Media;

OhosAudioCapturer::OhosAudioCapturer() : isCapturing_(false), audioCapturer_(nullptr), pcmFile_(-1) {}

bool OhosAudioCapturer::start(const std::string &filePath) {
    std::lock_guard<std::mutex> lock(mutex_);
    if (isCapturing_) {
        HILOG_ERROR(LOG_APP, "Already recording.");
        return false;
    }

    // 1. 配置并创建 AudioCapturer
    AudioCapturerOptions options;
    options.streamInfo.samplingRate = AudioSamplingRate::SAMPLE_RATE_44100; // 44.1kHz 采样率
    options.streamInfo.encoding = AudioEncodingType::ENCODING_PCM;
    options.streamInfo.format = AudioSampleFormat::SAMPLE_S16LE; // 16位深
    options.streamInfo.channels = AudioChannel::STEREO; // 立体声

    audioCapturer_ = AudioCapturer::Create(options);
    if (audioCapturer_ == nullptr) {
        HILOG_ERROR(LOG_APP, "创建 AudioCapturer 失败");
        return false;
    }

    // 2. 创建文件,准备写入 PCM 数据
    pcmFile_ = open(filePath.c_str(), O_CREAT | O_WRONLY | O_TRUNC, S_IRUSR | S_IWUSR);
    if (pcmFile_ < 0) {
        HILOG_ERROR(LOG_APP, "无法创建文件: %{public}s", filePath.c_str());
        audioCapturer_->Release();
        audioCapturer_ = nullptr;
        return false;
    }

    // 3. 启动录音
    if (audioCapturer_->Start() != 0) {
        HILOG_ERROR(LOG_APP, "启动 AudioCapturer 失败");
        close(pcmFile_);
        audioCapturer_->Release();
        audioCapturer_ = nullptr;
        return false;
    }

    isCapturing_ = true;
    // 开启一个线程循环读取音频数据
    captureThread_ = std::thread(&OhosAudioCapturer::captureLoop, this);
    HILOG_INFO(LOG_APP, "OHOS 音频录制已开始: %{public}s", filePath.c_str());
    return true;
}

void OhosAudioCapturer::captureLoop() {
    constexpr size_t bufferSize = 4096;
    uint8_t buffer[bufferSize];

    while (isCapturing_) {
        // 从 AudioCapturer 读取数据
        int32_t bytesRead = audioCapturer_->Read(buffer, bufferSize, false);
        if (bytesRead > 0) {
            write(pcmFile_, buffer, bytesRead); // 写入文件
            // 可以在这里计算当前缓冲区的振幅,供 getCurrentAmplitude 使用
            lastAmplitude_ = calculateAmplitude(buffer, bytesRead);
        } else if (bytesRead < 0) {
            HILOG_WARN(LOG_APP, "读取音频数据出错: %{public}d", bytesRead);
            break;
        }
    }
}

// stop(), release() 等方法需要确保线程安全并正确释放资源...
2.3.4 编写 Platform Channel 桥接层 (flutter_record_plugin.cpp)

这部分代码是“粘合剂”,负责接收 Dart 层的调用,并转给我们上面写的 OHOS 原生实现。

#include <flutter/plugin-interface.h>
#include <memory>
#include "ohos_audio_capturer.h"

using namespace flutter;

class FlutterRecordPlugin : public PluginInterface {
public:
    FlutterRecordPlugin() : audioCapturer_(std::make_unique<OhosAudioCapturer>()) {}

    void OnMethodCall(const MethodCall& call, const MethodResult& result) override {
        const auto& method = call.GetMethod();
        const auto* arguments = call.GetArguments();

        if (method == "startRecording") {
            if (!arguments || !arguments->IsString()) {
                result.Error("InvalidArguments", "需要提供文件路径字符串");
                return;
            }
            bool success = audioCapturer_->start(arguments->StringValue());
            result.success(success);
        } else if (method == "stopRecording") {
            bool success = audioCapturer_->stop();
            result.success(success);
        } else if (method == "getAmplitude") {
            double amplitude = audioCapturer_->getCurrentAmplitude();
            result.success(amplitude);
        } else if (method == "isRecording") {
            bool recording = audioCapturer_->isRecording();
            result.success(recording);
        } else if (method == "dispose") {
            audioCapturer_->release();
            result.success();
        } else {
            result.NotImplemented(); // 不认识的方法
        }
    }

private:
    std::unique_ptr<AudioCaptureInterface> audioCapturer_;
};

// 插件的创建和销毁入口函数
extern "C" FLUTTER_PLUGIN_EXPORT PluginInterface* CreatePlugin() {
    return new FlutterRecordPlugin();
}
extern "C" FLUTTER_PLUGIN_EXPORT void DestroyPlugin(PluginInterface* plugin) {
    delete plugin;
}

2.4 整合 Dart 层,看看怎么用

Dart 层的 API 我们尽量保持不动,这样原来的 Flutter 业务代码几乎不需要修改。

lib/flutter_record_plugin_ohos.dart:

import 'dart:async';
import 'package:flutter/services.dart';

class FlutterRecordPlugin {
  static const MethodChannel _channel =
      MethodChannel('flutter_record_plugin_ohos');

  /// 开始录音
  static Future<bool> startRecording({required String path}) async {
    try {
      final bool result = await _channel.invokeMethod('startRecording', path);
      return result;
    } on PlatformException catch (e) {
      print("启动录音失败: '${e.message}'.");
      return false;
    }
  }

  /// 停止录音
  static Future<bool> stopRecording() async {
    try {
      final bool result = await _channel.invokeMethod('stopRecording');
      return result;
    } on PlatformException catch (e) {
      print("停止录音失败: '${e.message}'.");
      return false;
    }
  }

  /// 获取当前音量振幅
  static Future<double> getAmplitude() async {
    try {
      final double amplitude = await _channel.invokeMethod('getAmplitude');
      return amplitude;
    } on PlatformException {
      return 0.0; // 出错就返回0
    }
  }

  /// 释放插件占用的资源
  static Future<void> dispose() async {
    try {
      await _channel.invokeMethod('dispose');
    } on PlatformException catch (e) {
      print("释放资源失败: '${e.message}'.");
    }
  }
}

在 Flutter 应用里,你可以这样调用:

import 'package:flutter_record_plugin_ohos/flutter_record_plugin_ohos.dart';

// 开始录音
bool started = await FlutterRecordPlugin.startRecording(path: '/data/app/recording.pcm');
if (started) {
  print('已经在 OHOS 上开始录音了!');
}

// 比如,每隔100毫秒获取一次振幅来更新UI
Timer.periodic(Duration(milliseconds: 100), (timer) async {
  double amp = await FlutterRecordPlugin.getAmplitude();
  _updateVolumeUI(amp); // 更新你的音量条
});

// 停止录音
bool stopped = await FlutterRecordPlugin.stopRecording();

三、 让插件更好用:优化和调试技巧

3.1 性能上需要注意的几点

  1. 管好内存:OHOS Native 层的 AudioCapturer 和文件描述符一定要在 release() 或析构函数里及时释放,这是避免内存泄漏的关键。
  2. 注意线程安全:录音循环跑在独立线程,像 isCapturing_ 这种共享状态,读写时必须加锁(比如用 std::mutex)。
  3. 按需调整参数AudioCapturerOptions 里的采样率、位深不是一成不变的。如果是录语音,可能用单声道、16kHz 就够了,能节省资源;录音乐则需要更高的质量。
  4. 考虑功耗:长时间后台录音要关注电量消耗和发热,可以适当调整读取数据的策略。

3.2 调试时可能会遇到的坑

  1. 善用日志:在原生代码里多使用 OHOS 的 HiLog 打印关键信息,在 DevEco Studio 的 Log 窗口里根据 TAG (FlutterRecordPlugin) 过滤查看,非常方便定位问题。
  2. 权限!权限!权限!:这是最容易出问题的地方。务必确认应用在真机或模拟器上已经获取了 MICROPHONE 等权限,否则录音会是静音的。
  3. 检查文件路径:确保你的应用有权限写入目标路径。最保险的做法是使用 OHOS 应用沙箱内的目录(比如通过能力上下文 context 获取的路径)。
  4. 先通通信:写个简单的 Dart 测试程序,把 start, stop, getAmplitude 等方法都调用一遍,确保 MethodChannel 通信本身是畅通的,参数传递也没问题。
  5. 验证录音结果:录下来的 PCM 文件可以用 Audacity 这类音频工具导入播放一下,确认声音内容是否正确,这是功能验证的最后一步。

四、 写在最后

通过上面这一系列步骤,我们成功让 flutter_record_plugin 在 OHOS 上“安家”了。经过测试,这个适配版插件可以在 OHOS 设备上稳定地进行音频采集,功能和原来的 Android/iOS 版本保持一致。

回顾整个适配过程,有这么几点体会:

  1. 吃透原理是关键:真正理解了 Platform Channel,适配工作就成功了一半。它就是个通信协议,我们的任务就是在 OHOS 端实现这个协议。
  2. 保持结构清晰:严格按照 Flutter 插件的标准格式来组织代码,把 OHOS 实现干净地放在 ohos/ 目录下,后期维护会轻松很多。
  3. 准确找到“替代品”:适配的本质是功能映射。在 OHOS SDK 里找到对等的原生 API(比如用 AudioCapturer 实现录音)是实现功能的核心。
  4. 稳健比功能多更重要:完善的错误处理、严格的资源生命周期管理、必要的线程同步,这些是保证插件在生产环境稳定运行的基石,一点都不能马虎。
  5. 这是一座“桥”:我们做的不仅是一个插件的移植,更是在 Flutter 生态和 OpenHarmony 生态之间搭起一座桥。这座桥通了,后面搬运其他优秀的库就会容易得多。

随着 OpenHarmony 自身能力的不断丰富,以及 Flutter 社区对 OHOS 支持的持续完善,我相信两者的结合会越来越紧密。希望这篇实践能给你提供一个清晰的路径,欢迎你一起探索,把更多好用的 Flutter 库带到 OpenHarmony 的世界里来。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值