更新记录

0.2.0(2026-08-26)

修复了已知的问题

0.1.0(2026-08-25)

新版本发布,暂时只支持android


平台兼容性

uni-app(4.0)

Vue2 Vue3 Chrome Safari app-vue app-nvue Android iOS 鸿蒙
× × × 8.0 ×
微信小程序 支付宝小程序 抖音小程序 百度小程序 快手小程序 京东小程序 鸿蒙元服务 QQ小程序 飞书小程序 小红书小程序 快应用-华为 快应用-联盟
× × × × × × × × × × × ×

yzj-builtin-cam

App nvue 内置相机 easycom 组件。Android 基于 CameraX、MediaCodec 与 RootEncoder;iOS 基于 AVFoundation。提供相机预览、拍照、带声音的 MP4 录像、前后镜头切换、对焦和补光灯。

支持传统 uni-app 的 App-AndroidApp-iOS nvue 页面,兼容 Vue 2 与 Vue 3。最低 Android API 26、iOS 12;Web、小程序和 HarmonyOS 不可用。

能力与限制

  • Android 支持拍照、录像和 RTMP 推流,可独立启停或组合运行;iOS 当前支持拍照与录像,RTMP 推流尚未实现。
  • Android 录像和推流共享一套 H.264 视频编码器、AudioRecord 和 AAC 编码器,避免重复占用相机与麦克风。
  • 默认视频参数为 1920x108030fps4000000bps;音频为 44100Hz、单声道、96kbps AAC-LC。
  • 组件宽高决定预览与照片的共同取景范围。推荐使用 16:9 容器。
  • 录像期间不能修改分辨率、帧率或切换镜头;Android 推流期间也遵循该限制。
  • 空路径时,Android 照片与录像分别保存到应用图片、视频目录;iOS 保存到 Documents/yzj_builtin_camera/。录像必须收到 recordchangestopped 且携带 path 后才能使用。

权限

Android 权限:

  • android.permission.CAMERA:打开相机、预览、拍照。
  • android.permission.RECORD_AUDIO:录像和 RTMP 推流采集声音。
  • android.permission.INTERNET:RTMP 推流。
  • android.permission.ACCESS_NETWORK_STATE:网络切换后自动恢复推流。

iOS 权限:

  • NSCameraUsageDescription:相机预览与拍照。
  • NSMicrophoneUsageDescription:录像同步采集声音。

Android 业务页面必须在调用 open() 前申请相机权限,在调用 startRecording()startRtmp() 前申请麦克风权限。iOS 组件会在首次 open()startRecording() 时请求对应系统权限;权限被拒绝时通过 error 返回错误码。

快速使用

在 nvue 页面直接使用 easycom 标签。组件应有确定宽高,推荐让宽高比与采集画幅一致。

<template>
  <view class="page">
    <yzj-builtin-cam
      ref="camera"
      class="camera"
      @statechange="onNativeEvent('statechange', $event)"
      @photo="onNativeEvent('photo', $event)"
      @recordchange="onNativeEvent('recordchange', $event)"
      @streamchange="onNativeEvent('streamchange', $event)"
      @error="onNativeEvent('error', $event)"
    />
    <button @click="openCamera">打开相机</button>
    <button @click="takePhoto">拍照</button>
    <button @click="toggleRecording">录像</button>
    <button @click="toggleStream">推流</button>
  </view>
</template>

<script>
export default {
  data() {
    return {
      recording: false,
      streaming: false,
      rtmpUrl: ''
    }
  },
  onUnload() {
    this.callCamera('releaseCamera')
  },
  methods: {
    getCamera() {
      return this.$refs.camera || null
    },
    callCamera(methodName, args) {
      const camera = this.getCamera()
      if (camera && typeof camera[methodName] === 'function') {
        camera[methodName].apply(camera, args || [])
      }
    },
    openCamera() {
      // 业务侧先确保已获得 CAMERA 权限。
      this.callCamera('open', ['back', 1920, 1080, 30, 4000000])
    },
    takePhoto() {
      this.callCamera('takePhoto', [''])
    },
    toggleRecording() {
      if (this.recording) this.callCamera('stopRecording')
      else {
        // 业务侧先确保已获得 RECORD_AUDIO 权限。
        this.callCamera('startRecording', [''])
      }
    },
    toggleStream() {
      if (this.streaming) this.callCamera('stopRtmp')
      else {
        // 业务侧先确保已获得 RECORD_AUDIO 权限。
        this.callCamera('startRtmp', [this.rtmpUrl])
      }
    },
    onNativeEvent(type, event) {
      const raw = event && event.detail != null ? event.detail : event
      const payload = typeof raw === 'string' ? JSON.parse(raw) : raw
      if (type === 'recordchange') {
        this.recording = payload.state === 'starting' || payload.state === 'started'
        if (payload.state === 'stopped') console.log('录像文件:', payload.path)
      }
      if (type === 'streamchange') {
        const activeStates = ['starting', 'connecting', 'authenticated', 'started', 'retrying', 'degraded']
        this.streaming = activeStates.indexOf(payload.state) >= 0
        console.log('RTMP 状态:', payload.state, payload.message)
      }
      if (type === 'photo') console.log('照片文件:', payload.path)
      if (type === 'error') console.error(payload.code, payload.message)
    }
  }
}
</script>

<style>
.page { flex: 1; }
.camera { width: 750px; height: 422px; }
</style>

Android 页面事件载荷通常为 JSON 字符串,应先读取 $event.detail 并解析;iOS 页面事件载荷为对象 Map。上例的 onNativeEvent() 已兼容两种格式。手动 import 的组件 ref 需要使用 $callMethod();easycom 标签可按上例调用公开方法。

Vue 2 nvue 示例

以下完整页面用于传统 uni-app Vue 2 的 pages/camera/index.nvue,覆盖打开关闭、能力查询、镜头、采集参数、对焦、补光、拍照、录像和 RTMP 推流。closeCamera(command)switchLens(command) 分别只是 close()switchCamera() 的兼容别名,因此不重复演示。

<template>
  <view class="page">
    <yzj-builtin-cam
      ref="builtinCam"
      class="camera"
      @statechange="handleCameraEvent('statechange', $event)"
      @capabilitieschange="handleCameraEvent('capabilitieschange', $event)"
      @photo="handleCameraEvent('photo', $event)"
      @recordchange="handleCameraEvent('recordchange', $event)"
      @streamchange="handleCameraEvent('streamchange', $event)"
      @error="handleCameraEvent('error', $event)"
    ></yzj-builtin-cam>

    <text class="status">{{ statusText }}</text>
    <view class="row">
      <button @click="openCamera">打开</button>
      <button @click="closeCamera">关闭</button>
      <button @click="switchCamera">切换镜头</button>
    </view>
    <view class="row">
      <button @click="useBackCamera">后置</button>
      <button @click="useFrontCamera">前置</button>
      <button @click="showCapabilities">能力</button>
      <button @click="showSession">会话</button>
    </view>
    <view class="row">
      <button @click="use1080p">1080p</button>
      <button @click="use720p">720p</button>
      <button @click="use24Fps">24fps</button>
      <button @click="use30Fps">30fps</button>
    </view>
    <view class="row">
      <button @click="use2Mbps">2Mbps</button>
      <button @click="use4Mbps">4Mbps</button>
      <button @click="enableAutoFocus">自动对焦</button>
      <button @click="focusCenter">中心对焦</button>
    </view>
    <view class="row">
      <button @click="toggleTorch">{{ torchEnabled ? '关闭补光' : '开启补光' }}</button>
      <button @click="takePhoto">拍照</button>
      <button @click="toggleRecording">{{ recording ? '停止录像' : '开始录像' }}</button>
    </view>
    <view class="row">
      <input class="rtmp-input" v-model="rtmpUrl" placeholder="rtmp://host:1935/live/stream"></input>
      <button @click="toggleStream">{{ streaming ? '停止推流' : '开始推流' }}</button>
    </view>
    <text v-if="photoPath">照片:{{ photoPath }}</text>
    <text v-if="videoPath">录像:{{ videoPath }}</text>
    <text v-if="lastError" class="error">{{ lastError }}</text>
  </view>
</template>

<script>
export default {
  data: function () {
    return {
      previewing: false,
      streaming: false,
      recording: false,
      torchEnabled: false,
      statusText: '等待打开相机',
      rtmpUrl: 'rtmp://your-host:1935/live/stream',
      photoPath: '',
      videoPath: '',
      lastError: ''
    }
  },
  onUnload: function () {
    this.callCamera('releaseCamera')
  },
  methods: {
    callCamera: function (methodName, args) {
      var camera = this.$refs.builtinCam
      if (camera && typeof camera[methodName] === 'function') {
        return camera[methodName].apply(camera, args || [])
      }
      if (camera && typeof camera.$callMethod === 'function') {
        return camera.$callMethod.apply(camera, [methodName].concat(args || []))
      }
      return null
    },
    requestPermission: function (permission, onGranted) {
      var self = this
      plus.android.requestPermissions([permission], function (result) {
        var granted = result && result.granted ? result.granted : []
        if (granted.indexOf(permission) >= 0) onGranted()
        else self.statusText = '权限被拒绝:' + permission
      }, function () {
        self.statusText = '权限申请失败:' + permission
      })
    },
    requestCameraPermission: function (onGranted) {
      this.requestPermission('android.permission.CAMERA', onGranted)
    },
    requestAudioPermission: function (onGranted) {
      this.requestPermission('android.permission.RECORD_AUDIO', onGranted)
    },
    openCamera: function () {
      var self = this
      this.requestCameraPermission(function () {
        self.lastError = ''
        self.statusText = '正在打开后置相机'
        self.callCamera('open', ['back', 1920, 1080, 30, 4000000])
      })
    },
    closeCamera: function () {
      this.statusText = '正在关闭相机'
      this.callCamera('close')
    },
    switchCamera: function () {
      this.callCamera('switchCamera')
    },
    useBackCamera: function () {
      this.callCamera('setLensFacing', ['back'])
    },
    useFrontCamera: function () {
      this.callCamera('setLensFacing', ['front'])
    },
    use1080p: function () {
      this.callCamera('setResolution', [1920, 1080])
    },
    use720p: function () {
      this.callCamera('setResolution', [1280, 720])
    },
    use24Fps: function () {
      this.callCamera('setFps', [24])
    },
    use30Fps: function () {
      this.callCamera('setFps', [30])
    },
    use2Mbps: function () {
      this.callCamera('setBitrate', [2000000])
    },
    use4Mbps: function () {
      this.callCamera('setBitrate', [4000000])
    },
    enableAutoFocus: function () {
      this.callCamera('setAutoFocus', [true])
    },
    focusCenter: function () {
      this.callCamera('focusAt', [375, 211])
    },
    toggleTorch: function () {
      this.torchEnabled = !this.torchEnabled
      this.callCamera('setTorch', [this.torchEnabled])
    },
    takePhoto: function () {
      this.callCamera('takePhoto', [''])
    },
    toggleRecording: function () {
      var self = this
      if (this.recording) {
        this.callCamera('stopRecording')
        return
      }
      this.requestAudioPermission(function () {
        self.callCamera('startRecording', [''])
      })
    },
    toggleStream: function () {
      var self = this
      if (this.streaming) {
        this.callCamera('stopRtmp')
        return
      }
      if (!this.rtmpUrl) {
        this.statusText = '请输入 RTMP 地址'
        return
      }
      this.requestAudioPermission(function () {
        self.callCamera('startRtmp', [self.rtmpUrl])
      })
    },
    showCapabilities: function () {
      console.log('相机能力:', this.callCamera('getCapabilities'))
    },
    showSession: function () {
      console.log('会话快照:', this.callCamera('getSessionSnapshot'))
    },
    decodeEvent: function (event) {
      var raw = event && event.detail != null ? event.detail : event
      if (typeof raw !== 'string') return raw || {}
      try {
        return JSON.parse(raw)
      } catch (error) {
        return { message: raw }
      }
    },
    handleCameraEvent: function (type, event) {
      var payload = this.decodeEvent(event)
      if (type === 'statechange') {
        this.previewing = payload.state === 'PREVIEWING'
        if (payload.state === 'CLOSED') {
          this.previewing = false
          this.streaming = false
          this.recording = false
          this.torchEnabled = false
        }
        this.statusText = payload.state || this.statusText
      }
      if (type === 'photo') {
        this.photoPath = payload.path || ''
        this.statusText = this.photoPath ? '照片已保存' : this.statusText
      }
      if (type === 'recordchange') {
        this.recording = payload.state === 'starting' || payload.state === 'started'
        if (payload.state === 'stopped') this.videoPath = payload.path || ''
        this.statusText = '录像状态:' + (payload.state || '')
      }
      if (type === 'streamchange') {
        var activeStates = ['starting', 'connecting', 'authenticated', 'started', 'retrying', 'degraded']
        this.streaming = activeStates.indexOf(payload.state) >= 0
        this.statusText = 'RTMP:' + (payload.message || payload.state || '')
        console.log('RTMP:', payload.state, payload.message)
      }
      if (type === 'error') {
        if (payload.operation === 'stream' || payload.operation === 'startRtmp') this.streaming = false
        if (payload.operation === 'recording' || payload.operation === 'startRecording') this.recording = false
        this.lastError = (payload.code || 'INTERNAL_ERROR') + ': ' + (payload.message || '')
        this.statusText = this.lastError
        console.error(payload.code, payload.message)
      }
    }
  }
}
</script>

<style>
.page { flex: 1; }
.camera { width: 750px; height: 422px; }
.row { flex-direction: row; margin-top: 12px; }
.row button { flex: 1; margin-right: 8px; }
.rtmp-input { flex: 1; height: 72px; border-width: 1px; border-color: #cccccc; }
.status { margin-top: 12px; font-size: 24px; }
.error { margin-top: 12px; color: #cc3333; }
</style>

属性

属性 类型 默认值 说明
initialLensFacing string back autoOpen=true 时使用的镜头,取值 backfront
width number 1920 autoOpen=true 时使用的采集宽度。
height number 1080 autoOpen=true 时使用的采集高度。
initialFps number 30 autoOpen=true 时使用的帧率。
initialBitrate number 4000000 autoOpen=true 时使用的视频编码码率,单位 bps。
autoOpen boolean false 组件原生视图加载完成后自动打开相机。

手势

相机预览区域支持以下手势:

  • 单指轻触:在触点位置执行自动对焦与测光,并显示白色焦点框缩小、停留、淡出的反馈动画。
  • 双指捏合:缩放倍率由当前设备摄像头能力限制。

相机未打开、已关闭自动对焦或正在切换前后镜头时,轻触对焦不会生效。手势操作不会中断录像或 RTMP 推流。

方法

方法 参数 说明
open(lensFacing, width, height, fps, bitrate) back/front, number, number, number, number 打开相机并设置初始视频参数。
close() 停止录像与推流,解绑相机;之后可再次 open()
closeCamera(command) 非空字符串 close() 的兼容别名。
release() Android 兼容方法:释放全部原生资源。iOS 不提供该方法,避免与 NSObject.release 冲突。
releaseCamera() Android 与 iOS 通用:释放全部原生资源;组件卸载时自动执行,释放后不可复用该实例。
getCapabilities() 返回 JSON 字符串,包含可用镜头、分辨率、补光灯与自动对焦能力。
getSessionSnapshot() 返回当前会话快照 JSON 字符串,适合调试。
switchCamera() 在前后镜头间切换。
switchLens(command) 非空字符串 switchCamera() 的兼容别名。
setLensFacing(lensFacing) back/front 指定目标镜头。
setResolution(width, height) number, number 设置下次编码使用的优选采集分辨率。
setFps(fps) number 设置下次编码使用的帧率。
setBitrate(bitrate) number Android:设置下次编码使用的视频码率,单位 bps。iOS:保留参数兼容性,实际录像码率由系统决定。
setAutoFocus(enabled) boolean 开启或关闭连续自动对焦。
focusAt(x, y) number, number 按组件坐标执行点对焦。
setTorch(enabled) boolean 控制后置摄像头补光灯。
takePhoto(outputPath) string 拍照;传空字符串时自动生成 JPG 路径。
startRecording(outputPath) string 开始带声音 MP4 录像;传空字符串时自动生成 MP4 路径。
stopRecording() 停止录像并异步完成 MP4 文件。
startRtmp(url) string Android:启动带声音的 RTMP/RTMPS/RTMPT/RTMPTS 推流。iOS:返回 RTMP_NOT_STARTED 错误。
stopRtmp() Android:主动停止 RTMP 推流。iOS:返回 RTMP_NOT_STARTED 错误。

setResolution()setFps()setBitrate() 必须在未录像、未推流时调用。调用后会重建采集配置;若设备不支持对应组合,将通过 error 返回失败。iOS 的 setBitrate() 不会强制改变 AVCaptureMovieFileOutput 的实际编码码率。

视频码率

initialBitrateautoOpen=true 时首次创建视频编码器所用的码率;setBitrate(bitrate) 用于在未录像、未推流时调整下一次编码会话的码率。单位均为 bps,例如 4000000 等于 4Mbps,不是 4000kbps4MB/s

视频码率同时影响 MP4 录像清晰度、RTMP 推流清晰度、文件大小和上行带宽占用。可按采集参数选择以下起始值:

分辨率与帧率 建议码率 适用情况
1280x720 @ 24fps 15000002000000 带宽受限或以流畅性优先。
1280x720 @ 30fps 20000003000000 常规移动网络推流。
1920x1080 @ 24fps 30000004000000 画面运动较少的高清录制或推流。
1920x1080 @ 30fps 40000006000000 默认高清推流场景。

网络不稳定时优先降低分辨率、帧率或码率。码率过高可能触发 RTMP_NETWORK_UNSTABLE,或导致服务端缓冲;码率过低会使运动画面出现明显压缩块。

照片与录像路径

takePhoto(outputPath)startRecording(outputPath) 都支持自动路径与自定义完整路径。iOS 自动路径位于应用 Documents/yzj_builtin_camera/,文件会通过事件 path 返回。

Android 传入空字符串时,组件使用应用外部私有目录自动生成文件:

操作 默认目录 文件名
takePhoto('') getExternalFilesDir(DIRECTORY_PICTURES)/yzj_builtin_camera/ photo_<时间戳>.jpg
startRecording('') getExternalFilesDir(DIRECTORY_MOVIES)/yzj_builtin_camera/ video_<时间戳>.mp4

可以传入完整本地路径指定文件名。目录必须可写,建议仍放在当前应用的外部私有目录中:

var packageName = 'uni.app.UNID0E57B9'
var photoPath = '/storage/emulated/0/Android/data/' + packageName + '/files/Pictures/inspection.jpg'
var videoPath = '/storage/emulated/0/Android/data/' + packageName + '/files/Movies/inspection.mp4'

this.callCamera('takePhoto', [photoPath])
this.callCamera('startRecording', [videoPath])

照片成功后通过 photo.path 返回最终路径。录像停止是异步操作,必须等待 recordchange 返回 state: 'stopped',并使用事件中的 path;在此之前 MP4 文件可能尚未完成索引,不能播放、上传或移动。

事件

事件 载荷 说明
statechange { state, operation, lensFacing } 相机状态,常见 PREVIEWINGCLOSED
photo { path } 照片已保存,path 为 JPG 本地路径。
recordchange { state, path } 录像状态:startingstartedstoppingstoppedfailed
streamchange { state, code, message, causeCode, stage, recoverable, rawReason, retryCount, retryMax } RTMP 状态与重试信息。
error { success, code, message, operation, causeCode, stage, recoverable, rawReason, retryCount, retryMax } 相机、音频、录像或推流失败。

capabilitieschange 是保留事件名;当前能力数据应通过 getCapabilities() 主动获取。

RTMP 重连与声音

RTMP 使用共享 H.264/AAC 编码结果。发生 Wi-Fi 与移动网络切换时,旧 TCP 连接报 Broken pipeConnection abortSocket closed 属于预期现象,不应将该单条底层日志当作应用崩溃。

  • 可恢复连接失败会淘汰旧 RtmpClient 并创建全新客户端,避免复用已经停止的 RootEncoder sender。
  • 每个连接使用独立 attempt ID,旧客户端回调不会终止新连接。
  • 存在网络时按 1.5 秒、2 秒、3 秒 退避重试,最多消耗 3 次服务端连接额度。
  • 没有具备 INTERNET 能力的活动网络时,每 3 秒等待网络恢复,不消耗服务端重试额度。
  • 新连接会等待视频关键帧,并缓存最新 AAC 样本,在首个关键帧后立即发送,避免重连后偶发无声。
  • RootEncoder 2.7.5 的客户端 ping 在断链竞态下可能导致未捕获异常,因此组件关闭该 ping,依赖服务端存活检测、读失败检测与连接回调处理断线。

业务侧应以 streamchange.state === 'started' 作为推流实际恢复的依据。retryingRTMP_WAITING_FOR_NETWORK 表示组件仍在自动恢复,不能主动再次调用 startRtmp()

常见错误码

错误码 处理建议
CAMERA_PERMISSION_REQUIRED 申请 CAMERA 权限后重新打开相机。
AUDIO_PERMISSION_REQUIRED 申请 RECORD_AUDIO 权限后重新开始录像或推流。
AUDIO_BUSY 上一麦克风会话仍在释放,稍后重试。
AUDIO_CAPTURE_FAILED / AUDIO_ENCODER_FAILED 检查麦克风是否被其他应用占用,或设备是否支持当前音频采集。
CAMERA_NOT_OPEN 先等待 statechange/PREVIEWING,再调用拍照、录像或推流。
CAMERA_COMBINATION_UNSUPPORTED 当前相机用例、分辨率或镜头组合不受设备支持。
RTMP_URL_INVALID 使用 rtmp://rtmps://rtmpt://rtmpts:// 地址。
RTMP_CONNECTION_TIMEOUT / RTMP_CONNECTION_REFUSED 检查服务器、端口、防火墙和网络。
RTMP_DNS_FAILED 检查域名、DNS 或网络可用性。
RTMP_PUBLISH_REJECTED / RTMP_AUTH_FAILED 检查流名称、鉴权参数及服务器发布权限。
RTMP_WAITING_FOR_NETWORK 等待 Wi-Fi 或移动网络恢复,无需停止后重开。
RTMP_RETRY_EXHAUSTED 网络正常但连续 3 次连接失败,检查服务端后由业务决定是否再次开始推流。
RTMP_AUDIO_PROCESSING_FAILED 检查 RTMP 连接和服务端音频接收能力。

完整错误载荷中的 code 为最终结果,causeCode 为原始分类,rawReason 保留底层异常信息。

录像、取景与资源说明

CameraX 负责预览、拍照和 ImageAnalysis;YUV 帧编码为 H.264,麦克风 PCM 编码为 AAC-LC。MP4 与 RTMP 共享编码结果,最后一个输出停止后才释放编码器与麦克风。

MP4 会等待音视频轨道均就绪,并从视频关键帧开始写入。手机断电、系统杀进程或应用崩溃时,正在写入的 MP4 可能没有完成索引;对关键业务建议采用固定时长分段录像。

预览、拍照和图像分析共享同一个 CameraX UseCaseGroup 与组件画幅。照片内容按组件画幅裁切;页面展示竖图时应根据图片实际比例设置容器,避免把竖图强制放入横向容器造成黑边。

使用注意事项

  • 不要在同一时间启动其他 CameraX 或 RootEncoder 相机 source,否则可能争抢同一摄像头。
  • 不要在收到 CLOSED、页面卸载或调用 release() 后继续调用组件方法。
  • stopRecording() 是异步收尾操作,收到 stopped 前不要使用文件或立即再次开始录像。
  • 停止最后一个录像或推流输出后立即重启,若收到 AUDIO_BUSY,等待旧麦克风会话释放后重试。
  • 推流重连期间不要重复调用 startRtmp();等待 started、最终 error 或业务主动 stopRtmp()
  • Android Studio 的 ViewRootImpl.dumpViewHierarchyProfileSaverBufferPoolManager 日志通常是布局检查、GC 或媒体缓冲统计,不等同于 RTMP 故障。排查推流优先关注 RtmpClientRtmpSenderTcpSocketCommandsManagerSocketException

真机验收

至少覆盖以下场景:

  1. 单独拍照、单独录像、单独推流。
  2. 拍照加录像、拍照加推流、录像加推流和三项同时运行。
  3. 录像、推流和两者同时运行时的前后镜头切换、停止、页面退出、快速停止后重启。
  4. Wi-Fi 推流后关闭 Wi-Fi,等待移动数据接管,确认状态最终回到 started 且服务端音视频均恢复。
  5. 移动数据推流后切回 Wi-Fi,重复验证视频、声音、本地录像文件和页面状态。

构建原生 AAR

修改 android-library/camera 下的 Kotlin 源码后,在项目根目录执行:

cd "uni_modules\yzj-builtin-cam\android-library"
.\build-and-copy-aar.ps1

脚本会构建 Release AAR、覆盖 utssdk/app-android/libs/yzj-builtin-cam-native-release.aar,并校验两份 AAR 的 SHA-256 一致。

随后必须重新制作并安装 Android 自定义调试基座。普通差量编译不能替换已安装基座中的 AAR。

如需手动构建:

cd "uni_modules\yzj-builtin-cam\android-library"
& "D:\Android\gradle\gradle-8.7\bin\gradle.bat" --no-daemon :camera:assembleRelease
Copy-Item ".\camera\build\outputs\aar\camera-release.aar" "..\utssdk\app-android\libs\yzj-builtin-cam-native-release.aar" -Force

实现结构

  • utssdk/app-android/index.vue:easycom 组件入口、公开方法与事件转发。
  • utssdk/app-android/native-view.uts:UTS 到原生 AAR 的桥接。
  • android-library/camera:CameraX、MediaCodec、MediaMuxer 和 RootEncoder 原生实现。
  • android-library/build-and-copy-aar.ps1:一键构建、复制与哈希校验脚本。
  • utssdk/app-android/libs/yzj-builtin-cam-native-release.aar:自定义基座实际加载的原生库。

隐私、权限声明

1. 本插件需要申请的系统权限列表:

android.permission.CAMERA:内置摄像头预览和拍照。 android.permission.RECORD_AUDIO:MP4 录像和 RTMP 推流采集声音。 android.permission.INTERNET:RTMP 推流。 android.permission.ACCESS_NETWORK_STATE:断网后等待网络恢复并自动重连。

2. 本插件采集的数据、发送的服务器地址、以及数据用途说明:

3. 本插件是否包含广告,如包含需详细说明广告表达方式、展示频率:

暂无用户评论。